Mini Project 6 09

완성된 배포 시스템, 운영 경험으로 재해석하기

많은 기능이 완료되었지만 무엇을 최종 성과로 말해야 하는지, 또 어떤 이슈가 단순 해프닝이 아니라 운영 경험인지 정리하기 어렵다.

근거 · 교안 안정성 테스트/최종 정리 파트

완성된 배포 시스템, 운영 경험으로 재해석하기 대표 이미지

1장: 요구사항, 체크리스트를 넘어 운영으로

솔라는 노트북 화면을 끄는 대신, 모니터 아래 포스트잇에 휘갈겨 쓴 목록을 물끄러미 바라봤다. 프로젝트는 거의 끝났다. 아니, 기능적으로는 완성되었다. 그런데 마지막 발표를 준비하려니 막막했다. 무엇을 성공적으로 마쳤다고 말해야 할까? 수많은 기능 목록과 버그 수정 기록이 눈앞에 어른거렸지만, 하나의 꿰어지는 이야기로 만들어지지 않았다.

특히 ‘문제 해결 경험 공유’라는 항목 앞에서 생각이 멈췄다. 크고 작은 문제들이 스쳐 지나갔다. 어떤 것은 하루를 꼬박 쓰게 했고, 어떤 것은 어이없는 오타 때문이었다. 그 목록을 전부 나열하는 건 단순한 해프닝 전시일 뿐, ‘운영 경험’이라고 말하기엔 왠지 낯간지러웠다.

“언니, 프로젝트 발표 준비하는데 좀 이상해.”

거실에서 책을 읽던 루나가 고개를 들었다. 솔라는 노트북과 포스트잇 뭉치를 들고 루나 옆에 앉았다.

“다 끝냈다고 생각했는데, 막상 뭘 발표해야 할지 모르겠어. 우리가 한 일은 엄청 많은데, 이게 그냥 기능 목록 나열이랑 뭐가 다른지….”

솔라는 화면에 띄운 프로젝트 교안의 최종 발표 요구사항을 보여주었다.

[최종 발표 요구사항]

  • 아키텍처 설명
  • CI/CD 파이프라인 데모
  • Auto Scaling / Load Balancing 데모
  • CloudWatch 모니터링 데모
  • 문제 해결 경험 공유

“이거 하나하나 다 구현했고, 데모도 할 수 있어. 그런데 이걸 그냥 따로따로 보여주는 게 맞을까? 꼭 숙제 검사받는 기분이야.”

루나는 솔라의 화면과 어지럽게 붙은 포스트잇을 번갈아 보았다. 포스트잇에는 ‘GitHub Actions 권한 문제 해결’, ‘EKS 클러스터 생성’, ‘ALB 연동’ 같은 구체적인 작업들이 적혀 있었다.

“솔라, 네가 지금 두 개의 목록을 보고 있네. 하나는 교안이 바라는 ‘결과물’의 모습, 다른 하나는 그 결과물을 만들기까지 네가 거쳐온 ‘과정’의 흔적들.”

루나는 잠시 생각하더니, 깨끗한 노트를 펼쳐 가운데 선을 그었다. 왼쪽에는 ‘요구사항’, 오른쪽에는 ‘우리가 한 일’이라고 적었다.

“이걸 체크리스트라고 생각하지 말고, 연결선 긋기 놀이라고 한번 해보자. 교안의 각 항목이 우리가 한 일 중 정확히 어떤 것과 이어지는지, 네가 직접 선을 그어보는 거야.”

솔라는 떨떠름한 표정으로 펜을 들었다. 이건 너무 뻔한 거 아닌가?

“일단… ‘아키텍처 설명’은 우리가 그린 아키텍처 다이어그램이겠지.” 솔라는 왼쪽에 ‘아키텍처 설명’을 쓰고, 오른쪽에 ‘AWS 서비스 구성도’라고 적어 선으로 이었다. “그리고 ‘CI/CD 파이프라인 데모’는 GitHub Actions랑 CodePipeline 설정한 거.” 금세 한 줄이 더 그어졌다.

하지만 ‘Auto Scaling / Load Balancing 데모’ 항목 앞에서 솔라의 펜이 잠시 멈칫했다. “음, 이건… Kubernetes HPA(Horizontal Pod Autoscaler) 설정한 거랑, Service를 LoadBalancer 타입으로 노출시킨 거.” 솔라는 ‘HPA 설정’, ‘K8s Service (type: LoadBalancer)‘라고 적었다. 하지만 아까와는 느낌이 달랐다. 이건 단순히 하나의 작업이 아니었다. 부하 테스트를 하고, CPU 사용량을 보면서 HPA 설정을 조정했던 기억, 로드 밸런서가 트래픽을 분산하는 로그를 확인하던 과정이 떠올랐다.

“조금 다르네.” 솔라가 혼잣말처럼 중얼거렸다. “앞의 것들은 그냥 ‘이거 만들었어요’ 하고 보여주면 되는데, 이건 ‘우리 시스템은 이렇게 동작해요’를 보여줘야 하는 거구나.”

루나는 말없이 고개를 끄덕였다.

솔라는 마지막 항목인 ‘문제 해결 경험 공유’를 노려보았다. 오른쪽 ‘우리가 한 일’ 칸에는 수많은 포스트잇의 내용이 스쳐 지나갔지만, 무엇을 적어야 할지 여전히 막막했다. “이건… 모르겠어. 너무 많기도 하고, 뭘 써야 할지.”

“방금 네가 발견한 차이가 힌트가 될 수 있어.” 루나가 입을 열었다. “어떤 항목은 ‘만들어진 것’ 자체를 보여주면 되지만, 어떤 항목은 ‘시스템이 어떻게 살아 움직이는지’를 보여줘야 한다고 했지? 그 살아 움직이는 과정에서 벌어졌던 일들 중에, 시스템의 동작 방식이나 한계를 명확하게 보여준 사건은 없었어?”

그 말에 솔라의 머릿속에서 한 장면이 번개처럼 스쳤다. 부하 테스트 중 HPA가 파드를 열심히 늘렸지만, 결국 데이터베이스 연결이 꽉 차서 시스템 전체가 느려졌던 아찔한 순간. 그건 단순한 실수가 아니었다. 시스템의 한 부분(웹 서버)의 확장이 다른 부분(데이터베이스)에 어떤 영향을 미치는지 온몸으로 깨달았던 사건이었다.

솔라는 펜을 들어 ‘문제 해결 경험 공유’ 옆에 이렇게 적었다. ‘HPA 확장과 RDS 연결 수 한계 문제’. 이것은 더 이상 해프닝 목록 중 하나가 아니었다. ‘Auto Scaling’이라는 요구사항이 현실 세계에서 어떻게 작동하고, 어떤 제약을 갖는지에 대한 생생한 증거였다.

솔라는 선으로 연결된 노트를 내려다보았다. 왼쪽의 추상적인 요구사항과 오른쪽의 구체적인 구현 목록, 그리고 그 사이를 잇는 경험들. 이것들은 더 이상 개별적인 점이 아니었다. 프로젝트라는 하나의 별자리를 이루는 선이었다.

“알겠다.” 솔라가 고개를 들었다. “성과는 우리가 만든 기능 목록이 아니었어. 이 요구사항들을 우리 시스템이 어떻게 ‘수행’하는지, 그 연결된 이야기를 보여주는 거였구나.”

이제야 발표의 첫 실마리가 잡힌 기분이었다. 하지만 동시에 새로운 궁금증이 고개를 들었다. 좋아, 요구사항과 구현을 연결하는 것까지는 알겠다. 그런데 그 수많은 구현 방법 중에 왜 우리는 하필 그 방법을 선택했을까? 예를 들어, 로드 밸런싱을 구현하는 여러 방법 중에서 왜 K8s Service 타입을 썼을까? 그 선택의 순간에는 어떤 고민이 있었을까? 그 배경을 설명할 수 없다면, 여전히 반쪽짜리 이야기가 될 것 같았다.

2장: 구현 선택, 왜 그렇게 했을까?

솔라의 시선은 어제 루나와 함께 정리했던 노트의 한 줄에 머물러 있었다. ‘Load Balancing 데모’라는 요구사항과 ‘K8s Service (type: LoadBalancer)’라는 구현을 이은 깔끔한 직선. 그 연결은 분명 사실이었지만, 보면 볼수록 무언가 빠진 듯한 허전함이 느껴졌다. 왜 우리는 그 방법을 썼을까? 다른 방법도 있었을 텐데.

솔라는 펜을 들어 ‘우리가 한 일’ 열의 오른쪽에 새로운 열을 만들었다. 그리고 머리글에 ‘선택 이유?’라고 적었다. 하지만 막상 내용을 채우려 하자 펜 끝이 망설여졌다. 가장 먼저 떠오르는 답은 ‘그게 제일 간단해서’였다. 하지만 발표 자리에서 그렇게 말할 수는 없는 노릇이었다. 그건 마치 고민 없이 되는대로 만들었다는 고백처럼 들릴 것 같았다. ‘선택 이유?’라는 질문 아래의 빈칸이 유독 커 보였다.

“언니, 이 연결선 말이야. 이게 그냥 사실을 나열한 것뿐이라면, 지난번과 다를 게 없는 것 같아.”

솔라가 새로운 고민이 담긴 노트를 루나에게 내밀었다.

“우리가 로드 밸런서를 구현한 방법이 K8s Service 타입을 쓴 거라고 연결은 했어. 그런데 왜 그 방법을 썼냐고 물으면, 뭐라고 대답해야 할까? 그냥 그게 쉬웠으니까? 분명 Ingress를 쓰는 방법도 있었고, 아예 수동으로 ALB를 구성하는 방법도 있었을 텐데. 우리의 선택을 설명할 말이 없어.”

그건 단순한 기능 구현을 넘어, 프로젝트의 ‘설계 의도’에 관한 질문이었다. 솔라의 말처럼, 선택의 배경을 설명할 수 없다면 그들의 이야기는 여전히 뼈대만 앙상한 보고서에 불과했다.

루나는 솔라가 새로 추가한 ‘선택 이유?’라는 빈칸을 가만히 들여다보았다.

“그럼 그 빈칸을 한번 채워보자. 우리가 하지 않았던 다른 방법들과 비교하기 전에, 우리가 했던 선택이 어떤 결과를 가져왔는지부터 따라가 보는 거야. ‘Service 타입을 LoadBalancer로 설정한다’, 이 한 줄의 코드가 우리 시스템에서 정확히 어떤 일을 했지?”

솔라는 잠시 프로젝트를 처음 구성하던 때를 떠올렸다. 수많은 YAML 파일과 씨름하던 기억.

“음… Service YAML 파일에서 type 필드를 ClusterIP에서 LoadBalancer로 바꾸고 적용했지. 그랬더니 AWS 계정에 로드 밸런서가 하나 뚝딱 생겼어. 정말 간단했지.”

“그다음은?”

“그다음? 그 로드 밸런서의 주소로 접속하니까 우리 프론트엔드 화면이 보였어.”

“로드 밸런서가 어떻게 우리 앱을 찾아갔을까? 그 중간 과정을 한번 그려볼래?”

루나의 말에 솔라는 노트의 빈 곳에 간단한 그림을 그리기 시작했다. 외부 인터넷을 상징하는 구름, AWS 로드 밸런서(LB) 아이콘, 그리고 쿠버네티스 클러스터 안에 있는 여러 개의 노드(워커 머신)와 그 안에서 실행 중인 파드(Pod)들.

[인터넷] ---> [AWS LB] ---> ??? ---> [파드]

“AWS LB에서 파드까지 바로 가진 않아. 클러스터의 노드 중 하나로 트래픽이 먼저 전달돼. NodePort라는 걸 통해서.” 솔라는 LB와 노드 사이에 화살표를 그리고 ‘NodePort’라고 적었다. “그리고 노드에 도착한 트래픽을 쿠버네티스가 다시 우리 서비스에 연결된 파드로 보내주는 거지.”

그림을 완성하고 보니, 아까는 보이지 않던 사실이 눈에 들어왔다.

“아…! 우리가 한 일은 YAML 파일 수정뿐이었지만, 쿠버네티스가 AWS와 통신해서 LB를 만들고, 외부 트래픽을 내부 파드까지 연결하는 복잡한 과정을 전부 알아서 처리해 준 거구나. 우리는 타겟 그룹을 설정하거나 헬스 체크를 수동으로 구성할 필요가 없었어.”

단순히 ‘쉬웠다’가 아니었다. 쿠버네티스의 선언적 방식이 주는 이점을 온전히 활용한 선택이었다. 그제야 ‘왜’라는 질문에 대한 첫 번째 단서가 보였다.

“맞아. 만약 우리가 Ingress를 썼다면 어땠을까?” 루나가 다음 질문을 던졌다.

“Ingress를 썼다면… 더 강력한 기능들을 쓸 수 있었겠지. 예를 들어, our-app.com/api는 백엔드로, our-app.com/는 프론트엔드로 보내는 경로 기반 라우팅 같은 거. 로드 밸런서 하나로 여러 서비스를 노출시킬 수도 있었을 거고. 하지만 우리 프로젝트는 프론트엔드 서비스 하나만 외부에 노출하면 됐으니까… 그럴 필요까지는 없었어. 오히려 Ingress Controller를 설치하고 관리하는 비용이 더 들었을 거야.”

순간, 머릿속에 흩어져 있던 조각들이 맞춰지는 기분이었다. 그들의 선택은 게으름의 결과가 아니라, 프로젝트의 요구사항과 범위에 정확히 부합하는 합리적인 트레이드오프였다.

솔라는 다시 펜을 들고 아까 비워뒀던 ‘선택 이유?’ 칸을 채워 넣기 시작했다.

요구사항구현선택 이유 (트레이드오프 분석)
Load Balancing 데모K8s Service (type: LoadBalancer)[목표] 단일 서비스를 외부에 안정적으로 노출.
[근거] Kubernetes 네이티브 방식으로 가장 빠르게 목표 달성 가능.
[장점] 선언적 설정(YAML)만으로 클라우드 인프라(LB) 자동 생성 및 연결. 관리 복잡도 낮음.
[단점/포기한 것] 복잡한 경로 기반 라우팅 불가. 서비스마다 LB가 필요해 비용 비효율적일 수 있음. (하지만 현 프로젝트 범위에서는 문제 되지 않음)

표가 완성되자, 단순한 기능 목록이 아니라 하나의 완결된 설계 결정처럼 보였다. 요구사항, 그에 따른 구현, 그리고 그 선택에 담긴 장점과 단점까지. 이 세 가지가 모여 비로소 ‘왜’라는 질문에 답하는 하나의 이야기가 되었다.

“이제 알겠어. 모든 기술 선택은 이런 식으로 설명할 수 있어야 하는구나. 우리가 무엇을 얻었고, 대신 무엇을 포기했는지. 그게 바로 우리 시스템의 맥락이니까.”

자신감이 붙은 솔라는 노트의 다른 항목들을 훑어보았다. 아키텍처, CI/CD 파이프라인… 각각의 선택에 대해서도 이런 분석을 더할 수 있을 것 같았다. 하지만 시선이 ‘문제 해결 경험 공유’ 항목에 닿자 다시금 미간이 찌푸려졌다. GitHub 권한 문제, Docker Hub 이미지 다운로드 제한, 프론트엔드와 백엔드 경로 충돌… 프로젝트 내내 크고 작은 문제들이 끊이지 않았다.

“좋아, 구현 선택의 이유는 이렇게 정리하면 되겠어. 그런데 우리가 겪었던 그 수많은 문제들은? 이것들도 다 의미가 있는 걸까? 아니면 그냥 지나가다 돌부리에 넘어진 해프닝일 뿐일까? 어떤 문제가 진짜 ‘운영 경험’이고, 어떤 게 그냥 ‘실수’인지 어떻게 구분하지?”

3장: 문제, 단순 해프닝에서 운영 경험으로

솔라의 책상 위, 어지럽던 포스트잇들은 간결한 디지털 목록으로 바뀌어 있었다. 하지만 정리는 또 다른 혼란을 낳을 뿐이었다. 솔라는 화면을 두 개의 열로 나누었다. 왼쪽 열의 이름은 ‘단순 해프닝’, 오른쪽 열은 ‘의미 있는 경험’. 프로젝트를 진행하며 마주쳤던 수많은 문제들을 이 두 분류에 꾸역꾸역 집어넣고 있었다.

‘GitHub 권한 문제’나 ‘Docker Hub 이미지 다운로드 제한’ 같은 것들은 ‘단순 해프닝’으로 보냈다. 외부 서비스의 정책이나 설정 실수처럼, 해결하고 나면 다시 돌아볼 필요가 없을 것 같았다. 반면 ‘HPA 확장 중 RDS 연결 초과 문제’는 ‘의미 있는 경험’ 칸에 넣었다. 왠지 모르게 중요해 보였기 때문이다. 하지만 왜 중요한지 명확히 설명할 수가 없었다. 두 열로 나뉜 목록은 깔끔해 보였지만, 그 분류 기준은 솔라 자신조차 설득하지 못하는 모래성과 같았다.

“언니, 이것 좀 봐봐. 우리가 겪었던 문제들을 정리하고 있는데, 더 이상해졌어.”

솔라의 부름에 다가온 루나는 화면 속 두 개의 열을 말없이 들여다보았다.

“분명히 어떤 문제는 그냥 지나가다 넘어진 돌부리 같고, 어떤 문제는 지도를 새로 그리게 만든 전환점 같은데… 그 둘을 나누는 기준을 모르겠어. ‘RDS 연결 초과 문제’가 ‘GitHub 권한 문제’보다 더 중요하게 느껴지는데, 왜냐고 물으면 그냥 ‘감’이라고밖에 말을 못 하겠어.”

솔라의 말대로였다. 해결에 걸린 시간이나 노력의 양으로 나눈 것도 아니었다. 어떤 문제는 순식간에 해결했지만 깊은 인상을 남겼고, 어떤 문제는 온종일 매달렸지만 다시 기억하고 싶지 않은 소모전처럼 느껴졌다.

루나는 솔라가 ‘의미 있는 경험’이라고 이름 붙인 열에 덩그러니 놓인 ‘HPA 확장 중 RDS Too many connections’ 항목을 가리켰다.

“그럼 이 문제의 이야기만 한번 해보자. 분류는 잠시 잊고, 그냥 그날 있었던 일을 시간 순서대로 말해주는 거야. 처음 증상을 발견했을 때부터.”

솔라는 잠시 눈을 감고 그 아찔했던 부하 테스트의 순간을 떠올렸다.

“부하 테스트를 시작했어. CPU 사용량이 치솟으니까 HPA가 정상적으로 동작해서 백엔드 파드(Pod) 수를 착착 늘리고 있었지. 모니터링 화면에 늘어나는 파드 수를 보면서 ‘됐다!’ 싶었는데… 갑자기 애플리케이션에서 ‘Too many connections’ 에러가 터져 나오기 시작했어.”

“그때 가장 먼저 뭘 의심했어?”

“음… 데이터베이스 설정 문제인 줄 알았어. 우리가 쓰는 RDS의 max_connections 파라미터가 너무 낮게 설정되어 있나 싶어서 확인했지. 그런데 그건 아니었어. 평소에는 충분한 값이었거든.”

바로 그것이 첫 번째 함정이자 오해였다. 솔라는 문제의 원인이 시스템의 한 부분(데이터베이스)에 고정된 값이라고 생각했다.

“계속 파고들다 보니 알게 됐어. 문제는 데이터베이스 설정값이 아니라, 우리 백엔드 애플리케이션이 작동하는 방식이었어. 새로 생성되는 파드 하나하나가 데이터베이스 커넥션 풀을 새로 만들면서 각자 연결을 점유했던 거야. HPA가 파드를 1개에서 10개로 늘리는 순간, 데이터베이스가 감당해야 할 연결 수도 10배로 폭증한 거지.”

그제야 솔라의 목소리에 깨달음의 빛이 스쳤다. 스케일링은 성공한 것처럼 보였지만, 사실은 시스템의 다른 쪽 끝을 무너뜨리고 있는 과정이었다. 웹 서버의 탄력성이 데이터베이스의 한계치를 폭력적으로 시험하고 있었다.

“그래서 어떻게 해결했지?”

“결국 HPA의 maxReplicas(최대 파드 수)를 데이터베이스가 감당할 수 있는 수준으로 제한했어. 무한정 늘어나는 게 능사가 아니라는 걸 깨달은 거지.”

이야기를 마치자, 루나가 조용히 질문을 던졌다.

“그 문제는 솔라 너에게 무엇을 ‘판단’하게 만들었어?”

“판단?”

“응. ‘버그를 고쳤다’에서 끝나지 않고, 그 사건 때문에 앞으로 우리가 이 시스템을 다룰 때 반드시 지켜야 할 새로운 규칙이나 제약을 만들게 된 것 말이야.”

그 질문에 솔라의 머릿속이 환해지는 기분이었다. 버그 수정은 과거를 향한 조치다. 하지만 판단은 미래를 향한 약속이었다.

“아…! ‘웹 서버는 필요할 때마다 얼마든지 늘릴 수 있다’는 생각이 틀렸다는 걸 알게 됐어. 그래서 ‘우리 시스템의 최대 확장 수는 데이터베이스 연결 수에 의해 제한된다’는 새로운 운영 규칙을 만들게 됐지. 그게 바로 우리가 내린 판단이었구나.”

솔라는 다시 화면을 보았다. ‘단순 해프닝’ 열에 있던 ‘GitHub 권한 문제’는 어떤가? 그 문제는 미래의 판단에 영향을 미쳤나? 아니었다. 그저 절차를 몰라서 생긴 일이었고, 해결 방법을 알게 된 후에는 더 이상 의사결정에 영향을 주지 않았다.

하지만 ‘HPA와 RDS 연결 문제’는 달랐다. 이 사건은 ‘Auto Scaling’이라는 요구사항을 다시 보게 만들었다. 그냥 ‘자동으로 늘어난다’는 기능 구현을 넘어, ‘우리 시스템이 감당 가능한 범위 안에서 안전하게 늘어난다’는 운영의 지혜를 가르쳐 주었다. 단순한 문제 해결이 아니라, 시스템의 한계와 특성을 정의하는 ‘판단’의 전환점이 된 것이다.

솔라는 키보드에 손을 올렸다. 그리고 ‘단순 해프닝’과 ‘의미 있는 경험’이라는 어설픈 열의 이름을 지웠다. 대신, 문제 목록 옆에 새로운 질문을 적었다.

‘이 문제가 우리에게 새로운 판단을 하도록 만들었는가?’

이 질문 하나로 모든 것이 명확해졌다. 어떤 문제가 단순한 해프닝이었고, 어떤 것이 뼛속 깊이 새겨야 할 ‘운영 경험’인지 가려낼 수 있는 명확한 필터가 생긴 것이다.

“이제 알겠어. 문제 해결 경험을 공유하라는 건, 그냥 고생담을 늘어놓으라는 게 아니었어. 어떤 문제들이 우리를 어떤 판단으로 이끌었는지, 그 전환점들을 이야기하라는 거였구나.”

솔라는 한결 가벼워진 마음으로 정리된 노트를 바라보았다. 요구사항과 그에 따른 구현 선택, 그리고 그 과정에서 겪은 ‘판단 전환점’으로서의 문제들. 이제 발표를 위한 강력한 재료들은 모두 모였다.

하지만 이 재료들을 어떻게 요리해야 할까? 요구사항, 구현, 그리고 문제 해결 경험. 이 세 가지를 따로따로 보여주는 게 아니라, 어떻게 하나의 흐름으로 엮어서 설득력 있는 ‘운영 사례’라는 이야기로 만들 수 있을까? 아직은 흩어진 구슬처럼 보였다.

4장: 완성된 운영 사례, 서사로 엮기

솔라의 책상 위는 발표 준비의 흔적으로 가득했다. 어지럽게 널려 있던 포스트잇은 깔끔하게 정리되었지만, 그 결과물은 영 마음에 들지 않았다. 화면에는 발표 슬라이드 초안이 떠 있었다. 1번 슬라이드는 ‘프로젝트 요구사항’, 2번은 ‘주요 구현 기술’, 3번은 ‘문제 해결 경험’. 각 슬라이드에는 지금까지 정리한 내용들이 빼곡히 들어차 있었다. 요구사항과 구현, 그리고 문제 해결 경험. 강력한 재료들이라고 생각했는데, 막상 따로따로 늘어놓으니 서로 아무런 관계가 없는 사실들의 나열처럼 보였다.

특히 ‘문제 해결 경험’ 슬라이드에 적힌 ‘HPA 확장 중 RDS 연결 초과 문제 및 maxReplicas 조정으로 해결’이라는 한 줄짜리 요약이 계속 눈에 거슬렸다. 이것은 분명 우리 팀이 겪은 가장 중요한 ‘판단 전환점’이었지만, 이렇게 툭 던져놓으니 그저 또 하나의 버그 수정 보고 같았다. 이 재료들을 어떻게 엮어야 하나의 의미 있는 요리가 될까. 솔라는 답답한 마음에 마른세수를 했다.

“발표 슬라이드 만드는 중이야?”

어느새 다가온 루나가 솔라의 모니터를 들여다보았다.

“응. 그런데 영 이상해. 분명히 다 중요한 내용들인데, 따로 노는 느낌이야. 요구사항은 요구사항대로, 구현은 구현대로, 문제는 문제대로… 이걸 듣는 사람이 우리가 뭘 해냈는지 제대로 이해할 수 있을까?”

솔라는 특히 ‘HPA와 RDS 문제’를 가리켰다. “이게 제일 중요한 경험 같은데, 그냥 ‘이런 문제가 있었고 이렇게 해결했다’고 말하는 게 전부일까? 지난번에 우리가 이야기했던 ‘판단 전환점’이라는 의미가 전혀 살지 않는 것 같아.”

루나는 잠시 생각하더니, 책상 위에 흩어져 있던 빈 인덱스 카드 몇 장을 가져왔다. 그리고는 펜을 들어 카드 네 장에 각각 짧은 질문을 적어 솔라 앞에 일렬로 늘어놓았다.

[우리의 목표는?][그래서, 뭘 만들었지?][무슨 일이 생겼나?][어떻게 마무리했나?]

“슬라이드 구성은 잠시 잊어버리자.” 루나가 말했다. “대신, 이 카드들의 빈칸을 채우면서 HPA 문제에 대한 하나의 이야기, 하나의 사례를 완성해 보는 거야. 이 사건의 주인공이 되어 시간 순서대로 여행을 떠나는 거지.”

솔라는 떨떠름한 표정으로 첫 번째 카드를 집어 들었다. [우리의 목표는?]

“우리의 목표… 그건 교안에 있던 ‘Auto Scaling 데모’ 요구사항을 충족시키는 거였지.” 솔라는 카드 아래에 ‘Auto Scaling 기능 구현 및 시연’이라고 적었다. 너무 당연한 시작이었다.

두 번째 카드, [그래서, 뭘 만들었지?]. “Kubernetes의 HPA(Horizontal Pod Autoscaler)를 백엔드 배포에 적용했어. CPU 사용량이 50%를 넘으면 파드 수를 자동으로 늘리도록.” 솔라는 ‘HPA 설정 (CPU 임계치 50%)’이라고 적었다. 여기까지는 지난번에 했던 ‘요구사항-구현 연결’ 작업과 크게 다르지 않았다.

하지만 세 번째 카드, [무슨 일이 생겼나?] 앞에서 솔라의 펜이 잠시 멈췄다. 지난번에는 단순히 ‘문제’라고만 생각했던 지점이었다. “부하 테스트를 시작하자 CPU 사용량이 치솟았고, HPA는 계획대로 파드를 늘렸어. 그런데 갑자기 애플리케이션 로그에 ‘Too many connections’ 에러가 쏟아졌지.”

“그걸 어떻게 알았지?” 루나가 슬쩍 끼어들었다.

“어떻게 알았냐니? 당연히… 아!” 솔라는 숨을 멈췄다. 머릿속에서 끊어져 있던 회로가 연결되는 느낌이었다. “CloudWatch 모니터링! ‘CloudWatch 모니터링 데모’도 교안 요구사항이었잖아! 우리는 그때 RDS의 데이터베이스 커넥션 수 지표를 보고 있었어. 파드 수가 늘어나는 것과 정확히 똑같은 그래프 모양으로 DB 커넥션 수도 급증하다가 한계치에 부딪히는 걸 봤어.”

솔라는 흥분하며 카드에 적어 내려갔다. ‘문제: HPA 확장 시 RDS 연결 고갈. (근거: CloudWatch 지표)’. 모니터링은 별개의 과제가 아니었다. 시스템의 이상을 감지하고 문제의 실체를 밝혀주는, 이야기의 핵심적인 탐정 도구였다.

마지막 카드, [어떻게 마무리했나?] “단순히 DB 연결 수를 늘리는 건 임시방편이었어. 우리는 시스템의 한계를 인정하고, HPA 설정에서 maxReplicas(최대 파드 수)를 우리 RDS가 안정적으로 감당할 수 있는 숫자로 제한했어.” 솔라는 마지막 카드에 ‘해결: maxReplicas 조정을 통한 안정화’라고 적었다.

솔라는 자신이 채워 넣은 네 장의 카드를 가만히 내려다보았다.

우리의 목표는?그래서, 뭘 만들었지?무슨 일이 생겼나?어떻게 마무리했나?
Auto Scaling 기능 구현HPA 설정 (CPU 50%)문제: RDS 연결 고갈
(근거: CloudWatch 지표)
해결: maxReplicas 조정

이것은 더 이상 분리된 사실의 목록이 아니었다. ‘Auto Scaling’이라는 요구사항으로 시작해, ‘HPA 구현’이라는 시도를 거쳐, ‘CloudWatch 모니터링’이라는 도구로 ‘RDS 연결 고갈’이라는 현실적인 제약을 발견하고, 마침내 ‘maxReplicas 조정’이라는 합리적인 해결책으로 시스템을 안정화시키는, 하나의 완결된 서사였다.

“이제 알겠어…” 솔라가 속삭였다. “문제 해결 경험을 공유하라는 건, 그냥 장애 보고를 하라는 게 아니었어. 요구사항, 구현, 모니터링, 문제가 어떻게 서로 맞물려 돌아가는지, 그 전체 과정을 하나의 ‘운영 사례’로 엮어서 보여주라는 거였구나.”

maxReplicas를 조정한 것은 단순한 버그 수정이 아니었다. 그것은 ‘우리 시스템의 Auto Scaling은 이런 방식으로 동작하며, 이러한 한계를 가지고 있다’는 것을 증명하는, 살아있는 이야기의 결말이었다.

자신감이 붙은 솔라는 슬라이드 초안을 모두 지웠다. 이제 발표의 구조가 보였다. 프로젝트를 몇 개의 핵심적인 ‘운영 사례 연대기’로 구성하면 되는 것이다.

하지만 문득 새로운 의문이 고개를 들었다. 이 HPA 사례는 정말 멋진 이야기다. 그런데 이 경험은 우리 프로젝트의 특정 기술 스택(Kubernetes, RDS)과 상황에 너무 깊이 묶여 있는 것 아닐까? 이 이야기에서 우리가 얻은 진짜 교훈은 뭘까? 다음 프로젝트에서 Kubernetes를 쓰지 않는다면, ‘maxReplicas를 조정했다’는 이 경험은 아무 쓸모가 없어지는 걸까? 완성된 사례로부터, 미래를 위한 보편적인 지혜를 어떻게 뽑아낼 수 있을지 막막했다.

5장: 다음 프로젝트를 위한 판단 추출

솔라의 모니터 화면 한쪽에는 ‘운영 사례 연대기’라는 이름으로 정리된 HPA 장애 사례가 펼쳐져 있었다. 하지만 화면 다른 쪽에 새로 띄운 빈 문서 앞에서 솔라의 손가락은 허공을 맴돌았다. 문서의 제목은 ‘다음 프로젝트를 위한 배포/운영 판단 리스트’. 과거의 경험을 미래의 자산으로 옮겨 담으려는 시도였다.

한참을 망설이던 솔라는 키보드에 손을 얹고 첫 번째 항목을 써 내려갔다. 1. HPA의 maxReplicas 설정 시, RDS의 max_connections 값을 반드시 고려한다. 문장은 사실이었지만, 무언가 잘못되었다는 느낌을 지울 수 없었다. 이 문장은 다음 프로젝트를 위한 ‘판단’이라기보다, 지난 프로젝트의 ‘결과 보고서’처럼 보였다. 솔라는 문장을 지우고 다시 썼다. 1. 쿠버네티스 환경에서 Auto Scaling을 구현할 때는 데이터베이스의 성능 한계를 함께 테스트해야 한다. 조금 나아졌지만, 여전히 답답함은 그대로였다. 이 교훈은 쿠버네티스와 RDS라는 특정 기술의 울타리 안에 갇혀 있었다.

“언니, 막혔어.”

솔라의 목소리에 막다른 길에 다다른 듯한 막막함이 묻어 있었다. 루나는 솔라가 썼다 지우기를 반복하는 화면을 조용히 들여다보았다.

“이 멋진 사례가 그냥 ‘쿠버네티스 사용 시 주의사항 1번’으로 끝나는 것 같아. 다음 프로젝트에서 서버리스를 쓰거나 다른 데이터베이스를 쓰면, 이 경험은 아무 쓸모없는 게 되어버릴까 봐 겁나.”

루나는 솔라가 지우려던 문장, ‘쿠버네티스 환경에서 Auto Scaling을 구현할 때는 데이터베이스의 성능 한계를 함께 테스트해야 한다’를 손으로 가리켰다.

“이 교훈들이 너무 구체적이라서 막히는 거네. 이 경험에서 기술 이름이라는 껍질을 벗겨내고, 다음에도 쓸 수 있는 보편적인 원칙만 추출해보자.”

루나는 노트를 가져와 두 개의 열을 그었다. 왼쪽에는 ‘우리가 겪은 일 (구체적 사실)’, 오른쪽에는 ‘그 일의 진짜 의미 (추상적 원칙)’라고 적었다.

“왼쪽 칸에 HPA 사례의 구체적인 사실을 다시 한번 적어봐. 기술 이름을 숨기지 말고, 있는 그대로.”

솔라는 펜을 들고 적었다.

우리가 겪은 일 (구체적 사실)

  • 백엔드 파드의 CPU 사용량이 늘자 HPA가 파드 수를 늘렸다.
  • 파드가 늘어나자 RDS의 DB 커넥션이 고갈되었다.
  • 결국 HPA의 maxReplicas를 조정하여 문제를 해결했다.

“좋아. 그럼 이제 오른쪽 칸을 채워보자.” 루나가 말했다. “이 사건의 본질은 뭘까? HPA, 파드, RDS 같은 이름을 다 지우고 나면 뭐가 남지?”

솔라는 왼쪽 칸의 문장들을 뚫어지라 쳐다보았다. ‘HPA가 파드를 늘렸다’… 이건 결국 ‘하나의 구성 요소가 동적으로 확장했다’는 뜻이었다. ‘RDS의 DB 커넥션이 고갈되었다’… 이건 ‘그 확장으로 인해 다른 구성 요소가 가진 고정된 한계에 부딪혔다’는 의미였다.

생각이 정리되자 솔라의 펜이 움직이기 시작했다.

그 일의 진짜 의미 (추상적 원칙)

  • 한 컴포넌트(웹 서버)는 부하에 따라 유연하게 확장했다.
  • 그 결과, 다른 컴포넌트(데이터베이스)의 정적인 한계치와 충돌했다.
  • 따라서, 한쪽의 유연성이 시스템 전체의 안정성을 보장하지 않는다는 것을 배웠다.

오른쪽 칸이 채워지자, 기술 이름에 가려져 있던 문제의 핵심 구조가 드러났다. 이것은 쿠버네티스만의 이야기가 아니었다. 서로 다른 특성을 가진 두 시스템이 연결될 때 언제든 발생할 수 있는 보편적인 갈등이었다.

“바로 그거야.” 루나가 오른쪽 칸을 가리켰다. “그럼 이 원칙을 바탕으로, 솔라 네가 다음 프로젝트의 설계 회의에 들어갔다고 상상해봐. 어떤 위험을 미리 막기 위해, 어떤 질문을 던지거나 어떤 원칙을 제안하겠어? ‘만약 X 라면, Y 해야 한다’는 형태로 한번 만들어볼래?”

그 순간, 솔라의 머릿속에서 모든 것이 연결되었다. 과거의 경험을 미래의 판단으로 바꾸는 마지막 열쇠를 찾은 기분이었다. 더 이상 HPA나 RDS 같은 구체적인 이름에 얽매일 필요가 없었다.

솔라는 다시 키보드 앞으로 돌아갔다. 그리고 아까의 답답한 문장들을 모두 지우고, 새로운 ‘판단 리스트’를 작성하기 시작했다.


다음 프로젝트를 위한 배포/운영 판단 리스트

판단 1: 한 컴포넌트의 탄력성이 다른 컴포넌트의 병목이 될 수 있음을 경계한다.

  • 상황: 특정 컴포넌트(예: 웹 서버, 서버리스 함수, 메시지 큐 컨슈머)의 자동 확장(Scale-out) 기능을 설계할 때.
  • 접근: 그 확장으로 인해 직접적인 부하를 받는 다른 컴포넌트(예: 데이터베이스, 외부 API, 파일 시스템)의 ‘고정된 한계’(최대 연결 수, Rate Limit, I/O 성능 등)를 반드시 파악하고, 그 한계치 안에서만 확장이 일어나도록 안전장치를 마련한다. 부하 테스트는 확장되는 컴포넌트뿐만 아니라, 부하를 받는 컴포넌트까지 함께 모니터링해야 한다.

판단 2: ‘가장 간단한 방법’은 ‘지금 포기하는 것’을 명시하는 대가로 선택한다.

  • 상황: 여러 기술적 대안 중 학습 곡선이 낮고 구현이 빠른 방법을 선택할 때 (예: Kubernetes Service LoadBalancer 타입 사용).
  • 접근: 그 선택으로 인해 단기적으로 얻는 ‘단순함’과, 그 대가로 장기적으로 포기하게 될 ‘확장성’ 또는 ‘기능’(예: 복잡한 경로 기반 라우팅, 비용 효율성)을 명확히 문서화하고 팀과 합의한다. 이는 미래에 기술 부채가 되었을 때, 과거의 의사결정 맥락을 이해하는 중요한 근거가 된다.

완성된 리스트는 더 이상 특정 기술의 사용법이나 문제 해결 기록이 아니었다. 그것은 어떤 기술 스택을 만나든 적용할 수 있는, 경험으로 증명된 강력한 의사결정 원칙이었다. 솔라는 자신이 작성한 두 개의 판단을 소리 내어 읽어보았다. 이제는 다음 프로젝트의 설계 회의에서 이 원칙들을 근거로 자신의 의견을 자신 있게 이야기할 수 있을 것 같았다. 프로젝트의 끝에서 얻은 가장 값진 성과였다.