Site Reliability Engineering (2016)
소프트웨어 시스템은 본질적으로 동적이며 불안정하다. 소프트웨어 시스템은 진공실에 들어있을 때만 완전히 안정적이다. 만일 우리가 기반 코드를 더 이상 변경하지 않는다면 더 이상의 버그도 나타나지 않을 것이다. 기반 하드웨어와 의존하는 라이브러리를 절대 바꾸지 않는다면 이런 컴포넌트들에서도 버그는 나타나지 않을 것이다. 현재 보유한 사용자 층을 더 이상 확대하려고 하지 않는다면 시스템을 확장할 필요도 없다.
결국 우리의 임무는 시스템의 신속함과 안정성 사이의 균형을 유지하는 것이다.
간혹 신속함을 위해 안정성을 희생할 수는 있다. 나는 간혹 익숙하지 않은 문제를 해결하기 위해 스스로가 '실험적 코딩(exploratory coding)'이라고 규정한 방법을 사용하곤 한다. 실험적 코딩이란 지금 내가 무슨 짓을 하고 있는지를 정확하게 이해하기 위해 일단은 실패할 것을 어느정도 예상하고 코드를 작성해보는 것이다. 유효한 날짜가 명확한 코드는 테스트 커버리지와 릴리즈 관리에서 조금 더 자유로울 수 있다. 왜냐하면 절대 프로덕션 환경에 배포되거나 사용자들의 눈에 띄는 일이 없기 때문이다. (중략)
주요 제품의 경우라면, 우리는 안정성과 신속함의 적절한 균형을 원할 것이다. 우리는 절차와 사례, 그리고 도구를 만들어 신뢰성을 구성하기 위해 일한다. 이런 작업이 개발자들의 신속성에 가능한 영향을 미치지 않도록 일한다.
신뢰성을 위한 프로세스는 개발자들의 신속성을 오히려 높이는 경향이 있다. 신속하고 신뢰할 수 있는 롤아웃은 좀 더 쉬운 환경의 변경을 보장한다. 그 결과, 일단 버그가 발견되면 더 빫은 시간 내에 버그를 찾아내어 수정할 수 있다. 개발 과정에 신뢰성을 주입하면 개발자들은 소프트웨어와 시스템의 기능과 성능에만 집중할 수 있다.
우리는 프로그램이 자생적이고 흥미로운 어떤 것이기를 원치 않는다. 스크립트 내에 가만히 앉아서 자신들의 비즈니스적 역할을 예측 가능한 상태에서 수행하기를 바랄 뿐이다. 구글 엔지니어인 Robert Muth의 말을 빌리면, "TV 수사물과는 다르게 재미도, 스릴도, 그리고 퍼즐도 없는 것이 소스코드의 바람직한 모습"이다.
Fred Books가 제안했듯이, 근본적인 복잡성과 돌발적인 복잡성을 구별하는 것이 중요하다. 근본적인 복잡성은 주어진 환경에서 문제의 정의로부터 제거할 수 없는 근본적인 것인 반면, 돌발적인 복잡성은 좀 더 유동적이며 엔지니어의 노력으로 해결이 가능하다.
예를 들어 웹 페이지를 서비스하기 위한 근본적인 복잡성을 가진 웹 서버를 작성한다고 가정해보자. 그러나 이 웹 서버를 자바로 개발한다면 가비지 컬렉션에 따른 성능 저하를 최소화할 때 돌발적인 복잡성을 마주하게 된다. 이런 돌발적인 복잡성을 최소화하기 위한 시각에서 다음을 준수해야 한다.
* 책임지고 있는 시스템에 있는 돌발적인 복잡성을 야기하는 요소는 과감히 밀쳐낸다.
* 자신이 담당하고 운영 책임을 지고 있는 시스템의 복잡도를 제거하기 위해 지속적으로 노력한다.
엔지니어 역시 사람이고 자신들의 창조물에 감정을 이입하는 경향이 있으므로 소스 트리의 대규모 정리에 대해 반감을 갖는 것은 그다지 몰라운 일이 아니다. 누군가는 "이 코드가 나중에 필요하면 어쩌려고?"라든가 "그냥 나중에 필요할 때 쉽게 돌려놓도록 주석 처리하는 게 어때?" 혹은 "지우지 말고 그냥 플래그만 달아두면 안 돼?"라며 항의하기도 한다. 전부 다 헛소리다.
소스 제어 시스템으로 변경을 쉽게 되돌릴 수 있는데, 수백 줄의 코드를 주석으로 처리하는 것은 코드를 산만하게 만들고 혼란을 가중시킬 뿐이다(특히 소스 파일이 계속 사용되는 경우에는 더욱 그렇다). 그리고 전혀 실행된 적이 없는 코드나 항상 비활성화 상태의 플래그가 설정된 코드는 마치 시한 폭탄 같은 것이다.
조금 과격하게 들릴 수도 있겠지만 24시간, 일주일 내내 끄떡없는 웹 서비스를 고려한다면 어느 정도까지는 새로 작성되는 코드 한 줄 한 줄이 모두 부채라고 생각해야 한다. 우리는 사례를 통해 분명한 목적이 있는 코드를 만들어 낸다. 예를 들면 서비스가 비즈니스 목표를 실제로 달성하고 있는지를 확인하는 코드나, 사용되지 않는 코드를 주기적으로 제거하는 코드, 혹은 모든 테스트 과정에서 자원을 과도하게 소모하는 부분을 탐지하는 등의 기능을 구현한다.
'소프트웨어 팽창(Software bloat)'이란, 소프트웨어가 시간이 지나면서 계속 추가되는 새 기능 때문에 점차 느려지고 비대해지는 현상을 의미한다. 본질적으로 이런 현상은 의도된 것은 아니지만 우리의 관점에서 볼 때 그 부정적인 영향은 명확하다. 프로젝트에서 변경되거나 추가된 모든 코드는 잠재적으로 새로운 결함과 버그를 수반할 수 있다.
규모가 작은 프로젝트라면 쉽게 이해하고 테스트할 수 있어 결함의 빈도가 낮다. 이런 관점을 염두에 둔다면, 프로젝트에 새로운 기능을 추가하도록 밀어붙이는 것을 조금은 유보할 수 있어야 할 것이다. 내 경험에서 가장 적절했던 코딩 중 하나는, 더 이상 유용하지 않은 수천 줄의 코드를 한 번에 삭제했던 것이다.
프랑스의 시인 생텍쥐페리는 "완벽함이란 더 이상 추가할 것이 없을 때가 아니라 더 이상 걷어낼 것이 없을 때 비로소 완성된다"라고 했다. 이 원리는 소프트웨어의 디자인과 구성에도 적용된다. API들은 왜 이런 규칙을 따라야 하는지 명확하게 보여주는 사례다.
조금 더 정확히 표현하면, API를 최소화하는 것은 소프트웨어 시스템의 간결함을 추구하기 위한 가장 기본적인 관점이다. 우리가 소비자에게 제공하는 API의 메소드와 매개변수를 줄인다면, 그 API는 더 이해하기 쉬울뿐더러, 우리는 그 API를 최대한 잘 만드는 데 더 많은 노력을 들일 수 있다.
다시 말하면, 여기에는 반복적으로 드러나는 테마가 있다. 특정 문제들이 발생하는 것을 막기 위한 적절한 결정을 통해 우리는 핵심 문제에 집중하고, 그에 대한 훨씬 나은 해결책을 모색할 수 있다. 소프트웨어에서는 더 적은 것이 오히려 더 나은 점이 많다! 작고 간결한 API는 그만큼 문제를 잘 이해했다는 보증수표이기 때문이다.
API에서 하나의 바이너리로 이야기의 범위를 넓혀보자. (중략) 특히 바이너리 간, 혹은 바이너리와 설정 간의 느슨한 결합은 개발자의 신속함과 시스템의 안정성을 동시에 향상시킬 수 있는 간소화 패턴이다. 대형 시스템의 컴포넌트 중 하나인 프로그램에서 버그가 발견된다면, 그 버그는 나머지 시스템과는 독립적으로 수정되어 제품 환경에 투입할 수 있다.
API들이 제공하는 모듈은 직관적인 것처럼 보이지만, 모듈화의 개념을 제대로 보여주지 못할뿐더러 API를 변경하는 방법으로 확대 적용하기도 어렵다. 한 API를 위한 변경사항 때문에 개발자는 전체 시스템을 다시 빌드하고 새로운 버그의 발생 가능성을 내포하게 된다.
API를 버전별로 관리하면 시스템이 예전 버전의 API를 사용하는 동안 개발자가 새로운 버전을 안전하고 적절한 방법으로 추가할 수 있다. 그렇게 함으로써 새로운 기능을 추가하거나 개선할 때마다 매번 전체 시스템을 프로덕션 환경에 배포하는 대신, 그때 그때 다른 리듬으로 릴리즈를 수행할 수 있다.
시스템이 복잡해져감에 따라 역할 분담에 대한 중요성은 더해져 간다. 관계가 없는 기능들을 모두 가지고 있는 하나의 큰 클래스를 작성하는 것은 좋지 않은 사례다. 잘 디자인된 분산 시스템은 명확하고 분명한 범위의 목적을 가진 바이너리들로 구성된다.
일반적으로 간결한 릴리즈가 복잡한 릴리즈보다 낫다. 여러 변경을 동시에 릴리즈하는 것보다는 하나의 변경을 릴리즈하면서 그 영향을 이해하고 분석하는 것이 훨씬 쉽다는 것은 자명하다. (중략) 릴리즈를 더 작은 단위로 수행할 수 있다면 더 강한 확신을 가지고 움직일 수 있다. 그 이유는 대형 시스템 내에 발생한 각각의 코드 변경에 대해 충분히 이해할 수 있기 때문이다.
우리는 이 방법을 통해 한 번에 작은 단위의 작업을 수행함으로써, 그 변경 결과가 개선을 ㅗ나타나는지 오히려 저하로 나타나는지를 파악하는 최적의 해결책을 찾을 수 있었다.
이 장에서 반복적으로 주장한 한 가지 테마는 바로 소프트웨어의 간결함은 신뢰성을 위한 사전 조건이라는 점이다. 우리는 주어진 작업의 각 단계를 간소화하는 방법을 찾는 데 게을리해서는 안 된다. 그 대신 무엇을 이루고자 하는지, 그리고 그것을 어떻게 하면 가장 쉽게 해결할 수 있는지를 명확히 해야 한다.
우리가 어떤 기능에 대해 "아니요"라고 답한다고 해서, 그것이 혁신을 거부하는 것은 아니다. 우리의 환경이 난잡해지지 않도록 유지하고 혁신에 정면으로 집중하면 진짜 엔지니어링을 할 수 있다.
- Max Luebbe 작성, 장현희 번역