기능을 넘어 운영을 만든다는 것
쉽게 보이는 기능과 그 뒤에서 제품이 책임져야 하는 것들
기능을 실행할 수 있다는 것과 제품으로 운영할 수 있다는 것 사이
제품을 만들다 보면 내부에서는 상당한 시간과 노력을 들인 기능이 외부에서는 의외로 단순하게 평가되는 경우가 있다.
“결국 설정값 하나 바꾸는 것 아닌가?”
“원래 있던 기능을 화면에서 쉽게 사용할 수 있게 만든 것 아닌가?”
표현은 조금씩 다르지만 질문의 본질은 비슷하다. 그리고 이런 질문이 완전히 틀렸다고 생각하지는 않는다.
실제로 많은 소프트웨어 기능은 가장 작은 단위로 분해하면 단순하다. 값을 변경하고, API를 호출하고, 프로세스를 실행하고, 이미 존재하는 시스템에 원하는 상태를 전달한다. 직접 명령을 실행하거나 설정 파일 몇 줄을 수정해도 같은 결과를 만들 수 있는 경우도 많다.
그렇기 때문에 제품의 가치를 설명하면서 “사실 이것은 굉장히 구현하기 어려운 기술이다”라고 답하는 것은 좋은 설명이 아니다. 구현 난이도와 제품의 가치는 같은 축이 아니다. 오히려 제품과 플랫폼을 오래 만들수록 더 크게 느끼는 차이가 있다.
기능을 실행할 수 있다는 것과 그 기능을 지속적으로 운영할 수 있다는 것은 서로 다른 문제다.
실행 가능성과 운영 가능성
기능을 가장 좁은 의미로 보면 질문은 간단하다.
할 수 있는가, 없는가.
어떤 자원을 생성할 수 있는가. 설정을 변경할 수 있는가. 작업을 실행할 수 있는가. 상태를 조회할 수 있는가. 이 수준에서는 명령 하나나 API 하나로 끝나는 일이 많다.
하지만 운영의 관점으로 넘어가면 질문이 달라진다.
누가 실행할 수 있는가. 지금 실행해도 되는 상태인가. 같은 요청이 동시에 들어오면 어떻게 할 것인가. 이미 사용 중인 자원과 충돌하지 않는가.
일부만 성공했다면 현재 상태를 무엇으로 판단할 것인가. 중간에 실패했다면 어디까지 되돌려야 하는가. 같은 요청을 다시 실행해도 안전한가. 변경된 상태를 다른 시스템은 언제부터 신뢰할 수 있는가. 사용이 끝난 자원은 언제, 어떤 기준으로 회수할 것인가.
이 시점부터 문제의 성격은 더 이상 특정 동작의 실행 여부에 머물지 않는다.
상태와 정책, 관계를 시간에 걸쳐 관리하는 문제가 된다.
기능의 구현 가능성은 특정 시점에 원하는 동작을 수행할 수 있는지를 묻는다. 운영 가능성은 그 동작을 다수의 사용자, 변화하는 상태, 실패와 경쟁 조건이 존재하는 환경에서 반복 가능하고 일관되게 유지할 수 있는지를 묻는다. 실제 제품에서는 대개 후자의 문제가 훨씬 오래 남는다.
Kubernetes가 좋은 예인 이유
이 차이를 설명하기에 Kubernetes만큼 익숙한 사례도 드물다. Deployment에 replicas: 3을 선언하는 행위만 보면 매우 단순하다. 세 개의 Pod가 필요하다는 값을 전달했을 뿐이다.
하지만 Kubernetes의 핵심은 그 값을 입력할 수 있다는 데 있지 않다. Kubernetes는 사용자가 선언한 spec, 즉 의도한 상태(desired state)와 실제 상태(current state)를 계속 비교하며 컨트롤러가 실제 상태를 의도한 상태에 가깝게 만든다.1
Pod 하나가 죽어 두 개가 되면 다시 세 개를 만든다. 노드 상태가 바뀌면 다시 배치한다. 사용자는 매번 현재 Pod 수를 확인한 뒤 부족한 만큼 직접 생성하지 않는다.
흥미로운 점은 여기서 사용자가 수행하는 가장 중요한 동작이 오히려 더 단순해졌다는 것이다.
replicas: 3
한 줄이다.
그렇다고 Kubernetes의 역할이 “Pod 세 개를 만드는 명령을 쉽게 만들어 주는 것”이라고 설명하지는 않는다. 중요한 것은 한 번 3을 만드는 것이 아니라 계속 3이도록 책임지는 것이기 때문이다.
이 차이는 제품을 이해하는 데 꽤 중요한 기준이라고 생각한다. 단발성 명령은 결과를 만든다. 운영 시스템은 결과가 유지되어야 할 조건과 상태까지 다룬다.
운영에는 시간이라는 축이 추가된다
단발성 작업과 운영의 가장 큰 차이 중 하나는 시간이다. 한 번 실행하는 작업은 그 순간의 입력과 상태만 정확히 알고 있으면 되는 경우가 많다. 운영은 그렇지 않다.
어제의 정상 상태가 오늘도 정상이라는 보장은 없다. 사용자는 추가되고 제거된다. 자원은 늘어나고 줄어든다. 정책은 바뀐다. 제품의 버전이 변경되고 그 아래에서 동작하는 플랫폼과 외부 시스템도 계속 변한다.
무엇보다 이미 실행 중인 상태가 존재한다. 그래서 운영 시스템은 과거의 작업 결과와 현재 상태, 앞으로 유지해야 할 상태를 동시에 다뤄야 한다.
설정 자체는 한 줄일 수 있다. 하지만 그 설정이 지금 적용 가능한지, 실제로 적용되었는지, 적용 도중 다른 작업에 영향을 주지는 않았는지, 실패했다면 현재 시스템을 어떤 상태로 판단해야 하는지는 한 줄의 문제가 아니다.
여기에 시간이 개입하면 단순한 기능은 상태 기계가 되고 상태 기계에는 전이와 실패, 재시도와 복구가 따라온다. 많은 엔지니어링 비용이 사용자가 보는 마지막 동작이 아니라 이 부분에서 발생한다.
규모가 커질 때 늘어나는 것은 대상보다 관계다
운영이 어려워지는 또 하나의 이유는 규모다. 다만 여기서 말하는 규모는 단순히 서버가 10대에서 100대로 늘어나는 식의 숫자 증가만을 의미하지 않는다.
대상이 세 개일 때는 사람이 기억할 수 있다. 열 개 정도도 운영 경험이 있는 담당자와 잘 정리된 문서가 있으면 관리할 수 있다. 하지만 대상이 수십, 수백 개가 되고 여러 사람이 동시에 사용하기 시작하면 기억과 수작업은 더 이상 신뢰할 수 있는 운영 방식이 되기 어렵다.
대상과 사용자 사이의 관계가 생기고 대상과 정책 사이의 관계가 생긴다. 한정된 자원을 두고 작업 간 경쟁이 발생하고 우선순위와 권한이 필요해진다. 한 요소의 변경이 다른 요소의 상태에 영향을 준다.
결국 규모가 커지면서 빠르게 증가하는 것은 대상의 개수보다 관계의 개수와 조합의 수에 가깝다.
운영 시스템은 바로 이 관계를 관리한다. 누가 무엇을 사용할 수 있는지, 무엇이 어디에 할당되어 있는지, 어떤 작업이 왜 대기하고 있는지, 현재 상태가 어떤 정책과 판단의 결과인지, 변경했을 때 무엇이 영향을 받는지. 개별 기능이 각각 존재한다고 해서 이 관계가 자동으로 관리되는 것은 아니다.
Google SRE가 말하는 toil
Google SRE에서는 운영 과정에서 반복적으로 발생하는 특정 종류의 일을 toil이라고 부른다. 단순히 하기 싫은 일을 뜻하지 않는다. 수작업이고, 반복적이며, 자동화할 수 있고, 전술적으로 처리되며, 지속적인 가치를 남기지 않고, 서비스 성장에 따라 선형적으로 늘어나는 일을 가리킨다.2
이 정의에서 특히 중요한 부분은 서비스가 커질수록 운영 작업도 같이 늘어난다는 점이다. 시스템이 두 배 커질 때 사람이 해야 할 확인과 조작도 두 배가 된다면 그 방식은 어느 순간 조직의 성장 속도를 제한한다.
Google SRE의 Carla Geisser가 남긴 표현은 꽤 직설적이다.
“If a human operator needs to touch your system during normal operations, you have a bug.”2
물론 현실의 모든 운영에서 사람을 제거할 수 있다는 의미로 받아들일 필요는 없다. 사람이 판단해야 할 일은 분명 존재한다.
중요한 것은 사람이 판단하는 것과 사람이 매번 같은 절차를 수행하는 것을 구분하는 데 있다. 제품이나 플랫폼이 가져가야 하는 것은 후자에 가까운 경우가 많다. 사람은 정책과 예외를 판단하고 시스템은 반복 가능한 절차와 상태 관리를 책임지는 편이 확장 가능하다.
나는 제품화의 중요한 목적 중 하나도 이 지점에 있다고 생각한다.
자동화와 운영 시스템도 같은 것은 아니다
그렇다면 스크립트를 만들어 자동화하면 운영 문제가 해결되는가. 일부는 해결된다. 수동으로 다섯 단계를 실행하던 작업을 스크립트 하나로 묶는 것만으로도 실수는 크게 줄어든다.
하지만 자동화와 운영 시스템 역시 같은 의미는 아니다. 스크립트는 보통 행위를 자동화한다. 운영 시스템은 행위뿐 아니라 상태와 정책을 관리한다.
예를 들어 특정 작업을 자동화한 스크립트가 있다고 해보자. 누가 실행해도 되는가. 이미 실행 중이면 다시 실행해도 되는가. 중간에 실패한 이전 실행은 어떻게 알 수 있는지, 현재 실제 상태가 스크립트가 기대하는 상태와 다르면 어떻게 할지, 한 번 성공한 결과가 이후에도 유지되고 있는지 누가 확인할지. 운영 시스템이 되려면 이 질문들에 대한 모델이 필요하다. 결국 명령을 사람 대신 실행하는 것과 시스템이 운영 책임을 가져가는 것 사이에는 한 단계가 더 있다.
배포도 deploy 한 번으로 끝나지 않는다
소프트웨어 배포 역시 비슷하다. 코드를 서버에 올리는 동작 자체는 오래전부터 어렵지 않았다. 파일을 복사할 수도 있고, 명령 하나로 배포할 수도 있다.
그런데 소프트웨어 업계는 그 위에 빌드 시스템, CI/CD, release engineering이라는 상당히 큰 영역을 만들었다. 이유는 배포 명령이 어려워서가 아니다. 같은 소스에서 같은 결과를 재현할 수 있어야 하고 반복해서 같은 방식으로 배포할 수 있어야 하며 테스트와 검증을 거쳐야 한다. 문제가 생겼을 때 현재 rollout 상태를 알아야 하고 필요하면 이전 상태로 되돌릴 수 있어야 하기 때문이다.
Google의 Release Engineering 문서도 신뢰할 수 있는 서비스 운영을 위해 빌드와 설정이 재현 가능하고 자동화되어야 하며, 릴리스가 반복 가능해야 한다는 점을 강조한다.3
한 번 배포하는 능력과 계속 안전하게 배포하는 능력은 다르다. 기능과 운영의 차이도 이와 크게 다르지 않다.
정상 경로보다 예외 경로가 제품을 만든다
제품 개발에서 정상적으로 성공하는 첫 번째 경로를 만드는 데 필요한 시간은 전체 개발 시간의 일부일 때가 많다. 실제 완성도를 결정하는 것은 그 주변이다.
동시에 두 사람이 같은 작업을 요청했을 때, 외부 시스템 호출은 성공했지만 내부 상태 저장에 실패했을 때, 일부 대상에는 적용되고 일부에는 실패했을 때. 오래 걸리는 작업이 중간에서 멈췄거나 이전에 실패한 작업을 다시 시도했을 때. 제품 버전이나 외부 API가 변경되었을 때.
운영되는 제품이라면 이런 상황에 대해 나름의 답이 있어야 한다. 그리고 답은 예외 메시지 하나를 보여주는 것으로 끝나지 않는다.
현재 상태를 무엇으로 정의할지 결정해야 한다. 자동으로 복구할지, 사용자가 재시도할지, 운영자가 개입할지 판단해야 한다. 사람이 판단해야 한다면 어떤 정보까지 제공할지도 결정해야 한다.
이런 과정은 정상적으로 동작할 때는 거의 드러나지 않는다. 사용자는 성공한 한 번의 동작을 본다. 제품을 만드는 쪽에서는 그 한 번의 동작이 실패할 수 있는 여러 경로와 실패 후의 상태까지 함께 본다. 만드는 사람과 사용하는 사람이 느끼는 기능의 크기가 다른 이유 중 하나다.
제품화는 복잡성을 없애기보다 책임의 위치를 바꾸는 일이다
나는 좋은 제품이 복잡성을 “없앤다”는 표현을 완전히 믿지는 않는다. 없앨 수 있는 복잡성도 있지만 문제 자체에 포함된 본질적인 복잡성은 남는다.
제한된 자원은 누군가 배분해야 하고 권한은 결정되어야 한다. 충돌하는 요청에는 우선순위가 필요하다. 실패한 작업의 상태는 정의되어야 하고 사용이 끝난 자원은 회수되어야 한다. 이런 복잡성은 UI를 단순하게 만든다고 사라지지 않는다.
달라지는 것은 누가 그 복잡성을 책임지는가다.
제품이 충분한 역할을 하지 못하면 복잡성은 사용자에게 남는다. 사용자가 문서를 읽고 실행 순서를 기억한다. 여러 시스템에 접속해 현재 상태를 비교하고 문제가 생기면 경험이 많은 담당자를 찾는다. 조직 안에는 문서로 남기 어려운 암묵적인 운영 규칙이 쌓이고 담당자가 바뀌면 그 지식도 다시 전달해야 한다.
반대로 제품이 더 많은 책임을 가져가면 사용자가 감당하던 복잡성의 일부가 제품 내부로 이동한다. 제품이 현재 상태를 기억하고 정책을 일관되게 적용한다. 유효하지 않은 조합을 막고 변경 이력을 남긴다. 실패한 상태를 드러내고 필요한 경우 다시 정상 상태로 수렴시킨다. 여러 사용자의 행위를 하나의 운영 기준 안에서 정리한다.
이 관점에서 제품화는 기능을 예쁜 화면으로 포장하는 일이 아니다. 사용자가 직접 소유하던 운영 책임 중 어디까지를 제품의 책임으로 가져올 것인지 결정하는 일에 가깝다.
이것은 결국 control plane의 문제이기도 하다
인프라 시스템을 보면 이 구조가 더 분명하게 드러난다. 여기서는 대개 두 영역을 나눈다. 하나는 실제 패킷을 전달하거나 컨테이너를 실행하거나 자원을 소비하는 영역이다. 다른 하나는 무엇을 어디에 배치하고 어떤 정책을 적용하며 현재 상태를 어떻게 유지할지를 결정하는 영역이다. 흔히 후자를 control plane이라고 부른다.
사용자는 최종 행위를 하나하나 직접 지시하지 않는다. 원하는 상태와 정책을 표현하고 control plane이 실제 상태를 관찰하면서 필요한 동작을 만들어 낸다. Kubernetes의 controller/reconciliation 구조도 대표적인 예다.
이 관점으로 보면 많은 소프트웨어와 플랫폼 제품이 왜 외부에서는 생각보다 단순하게 보이는지도 설명할 수 있다. 사용자가 직접 실행해야 했던 수많은 조작이 제품 내부의 control plane으로 이동했기 때문이다. 사용자가 보는 마지막 조작은 작아졌지만 제품이 맡는 책임은 오히려 커진다.
그렇다고 내부의 복잡함 자체가 가치인 것은 아니다
여기에는 중요한 경계가 있다. 제품 내부에 복잡한 코드와 상태가 많다고 해서 그것이 곧 제품의 가치가 되는 것은 아니다. 잘못된 설계 때문에 복잡할 수도 있다.
불필요한 추상화, 일관되지 않은 모델, 과도한 일반화, 기술 부채 때문에 내부 구현이 복잡해졌다면 그것은 고객에게 설명할 가치가 아니라 우리가 제거해야 할 비용이다. “밖에서는 단순해 보이지만 안에서는 복잡하다”는 논리는 쉽게 내부의 복잡성을 합리화하는 말이 될 수 있다.
그래서 중요한 것은 복잡함의 양이 아니라 복잡함의 출처라고 생각한다.
문제 도메인에 본질적으로 존재하고 사용자가 매번 직접 다룰 필요가 없는 복잡성을 제품이 가져갔다면 가치가 있다. 반대로 제품의 내부 사정 때문에 만들어진 복잡성은 가능한 한 제거해야 한다.
좋은 제품은 복잡한 시스템이라기보다 필요한 복잡성을 올바른 위치에 배치한 시스템에 더 가깝다.
단순한 사용 경험은 결과이지 본질이 아니다
“결국 사용하기 편하게 만든 것 아닌가?”라는 질문도 같은 기준으로 볼 수 있다. 어떤 경우에는 정확한 지적이다. 명령어 하나를 버튼 하나로 옮겼을 뿐이고, 사용자가 여전히 실행 조건을 모두 직접 판단해야 하며, 실패하면 내부 시스템에 들어가 수동으로 복구해야 한다면 실제로 편의 기능에 가깝다.
UI가 생겼다는 이유만으로 운영 시스템이 되는 것은 아니다. 반대로 사용 방법이 단순하다는 이유로 제품의 가치를 낮게 볼 필요도 없다. 잘 만들어진 제품은 사용자가 반드시 알아야 할 판단만 남기고 나머지 절차를 제품의 책임으로 가져가는 방향으로 발전한다.
전자결재 시스템을 생각하면 이해하기 쉽다. 사용자의 마지막 동작은 승인이나 반려다. 그렇다고 전자결재 시스템의 가치가 두 개의 버튼에 있다고 생각하지 않는다. 시스템이 그 뒤를 관리하기 때문이다. 누가 승인해야 하는지, 어떤 순서로 승인해야 하는지, 현재 어디까지 진행되었는지, 누가 언제 어떤 결정을 했는지, 조직의 정책이 동일하게 적용되고 있는지를 관리한다.
사용자의 마지막 동작이 단순하다는 것은 그 앞의 과정이 존재하지 않는다는 뜻이 아니다. 오히려 시스템이 그 과정을 충분히 책임지기 때문에 마지막 판단만 남을 때가 많다. 제품의 복잡도를 UI의 클릭 수로 판단하는 것은 적절하지 않다.
봐야 하는 것은 클릭 뒤에서 제품이 맡고 있는 책임의 범위다.
모든 운영을 제품으로 만들 필요는 없다
반대쪽도 같이 봐야 한다. 운영이 존재한다고 해서 언제나 시스템화해야 하는 것은 아니다. 대상이 적고 사용자가 몇 명 되지 않으며 변경 빈도도 낮고 운영 담당자가 명확하다면 사람이 직접 처리하는 편이 더 경제적일 수 있다.
Google SRE가 toil을 중요하게 다루는 것도 모든 수작업을 무조건 자동화하라는 의미는 아니다. 자동화 자체에도 개발과 유지 비용이 있고 작은 규모에서는 그 비용이 수작업보다 클 수 있다.
그래서 중요한 것은 자동화 여부보다 반복성과 규모, 운영 비용의 증가 방식이라고 생각한다.
대상이 하나 늘어날 때마다 관리 비용도 거의 같은 비율로 늘어나는가. 사람이 늘어날수록 조정 비용이 지속적으로 증가하는가. 같은 일을 사람마다 다른 방법으로 처리하고 있지는 않은지, 특정한 몇 사람만 전체 맥락을 알고 있지는 않은지도 짚어볼 만하다. 과거 작업 결과를 확인하기 위해 메신저와 문서를 다시 찾아야 하는지, 장애가 날 때마다 경험 많은 사람이 직접 개입해야 하는지도 같이 본다. 이런 일이 반복되기 시작하면 개별 기능보다 운영 체계를 고민해야 할 시점에 가까워진다.
시스템화는 기술적인 욕망 때문에 하는 것이 아니다. 수작업의 한계가 실제 비용과 리스크로 나타나기 시작할 때 필요해진다.
기능이 아니라 책임의 범위를 본다
결국 이 문제를 생각하면서 다시 보게 된 것은 제품의 책임 범위다. 기능을 제공한다는 것은 가장 좁게 보면 특정 동작을 가능하게 하는 일이다. 제품을 만든다는 것은 책임의 폭이 그보다 넓다.
그 기능을 누가 사용할 수 있는가. 언제 사용할 수 있는가. 어떤 정책 아래에서 동작하는가. 실행 전후의 상태는 무엇인가. 실패했을 때 어떻게 복구하는가. 여러 사용자가 동시에 사용해도 같은 기준이 유지되는가. 시간이 지나 환경이 달라져도 계속 운영할 수 있는가.
제품이 이런 질문에 대한 답이 많을수록 사용자는 밑단의 기술보다 자신이 해결하려는 문제에 집중할 수 있다. AWS Well-Architected Framework가 Operational Excellence를 단순한 시스템 구축이 아니라 규모 있게 운영하고, 상태를 파악하며, 운영 절차를 계속 개선하는 능력까지 포함해 정의하는 것도 같은 맥락으로 읽힌다.4
좋은 제품이 쉬워 보이는 것은 이상한 일이 아니다. 오히려 그래야 한다. 다만 그 단순함이 명령을 버튼으로 옮긴 결과인지, 아니면 제품이 실제로 복잡한 운영 책임을 가져간 결과인지는 구분할 필요가 있다.
내가 생각하는 제품의 가치는 후자에 있다.
기능을 실행하는 방법을 제공하는 것을 넘어, 그 기능이 계속 올바르게 사용될 수 있는 조건과 상태를 제품이 책임지는 것.
기능과 제품 사이의 경계를 어디에서 나눌 것인지 묻는다면 지금은 그 지점을 먼저 떠올릴 것 같다.
참고
-
Kubernetes Documentation, Controllers / Objects In Kubernetes. Kubernetes는 컨트롤러를 통해 현재 상태를 사용자가 선언한 의도한 상태에 지속적으로 가깝게 만든다. ↩
-
Google, Site Reliability Engineering, Eliminating Toil. SRE에서는 toil을 수작업·반복·자동화 가능성·전술성·지속 가치 부재·서비스 성장에 따른 선형 증가 등의 특성으로 설명한다. ↩ ↩2
-
Google, Site Reliability Engineering, Release Engineering. Google의 Release Engineering은 재현 가능하고 자동화된 빌드와 일관되고 반복 가능한 릴리스 절차를 신뢰성의 중요한 조건으로 다룬다. ↩
-
AWS, Operational Excellence Pillar - AWS Well-Architected Framework. Operational Excellence를 조직, 워크로드 설계, 규모 있는 운영, 지속적인 개선을 포함하는 능력으로 정의한다. ↩