Erik Bernhardsson
2017-07-06
나는 전에 "빠른 이터레이션의 중요성" 이란 기고를 작성했었다. 하지만 당신이 할 수 있는 구체적인 일들에 관해 이야기할 필요는 없었다. 내가 베터 닷컴에서 기술팀을 조직할 때 무엇보다도 빠른 이터레이션 속도를 의도적으로 최적화했었다. 그렇게 했던 방법이 무엇이었을까?
지속적인 배포
나는 우리가 전 세계에서 지속 배포하는 유일한 금융 기관이라고 말하고 싶다. 실제로 이코노미스트 잡지에서 이에 대해 구체적으로 인용했었다. 우리는 아마 50-100번가량 매일 실제 환경에 배포한다. 하나의 풀 리퀘스트가 마스터에 머지되고 나면 수 천개의 유닛 테스트와 수백 개의 셀레니움 테스트로 된 상당히 광범위한 테스트 스위트를 실행한다. 우리는 이 테스트를 수행하는 데 걸리는 시간을 최적화하기 위해 상당한 시간을 소비했다. 그래서 정말로 약 15분 만에 끝난다. 만일 모든 테스트를 통과하면 우리는 그걸 실제 환경에 배포한다.
* 풀 리퀘스트 (pull request): 소스 작업에서 주 저장소에 내 작업 브랜치를 머지해 달라는 요청. 작업 방식에 따라 담당자가 바로 주 저장소에 머지하거나 리뷰 과정 이후에 머지하는 단계가 존재한다. 또한 원격 저장소에 내 작업을 푸시 했다는 사실을 알리는 역할도 한다. |
우리는 지속적인 통합을 위해 Buildkite를 사용하며 모든 서비스는 배포하는 동안 다운타임이 없도록 하기 위해 블루/그린 배포 환경을 지원하는 (수많은 다른 것 중에서) 쿠버네티스 위에서 돌아간다.
테스팅
지속적인 배포는 책임을 지는 자유이며 엄격한 테스팅 없이는 가능하지 않다. 우리는 대략 85%의 유닛 테스트 커버리지를 (내 생각에 최적 지점은 90% 정도이며 100%는 현실적이지 않다고 생각한다) 달성한다. 대체로 하나의 기능이 한동안 실 배포 환경에 이미 나가 있을 때 제품 관리자만 그 기능이 스펙에 맞는지 확인하기 위해 수동 테스팅을 수행한다.
우리가 실 환경에 버그를 릴리즈한 적이 있었을까? 물론이다. 하지만 복구하는데 걸리는 평균 시간이 대체로 MTBF (평균 고장 간격) 시간보다 중요하다. 만일 우리가 뭔가 잘못된 걸 배포 했다면 우리는 수 분 내에 롤백할 수 있다. 그리고 우리는 매우 세밀한 증분을 출시하기 때문에 평균적인 버그가 미치는 영향력이 제한된다. 실 환경의 버그는 지난 며칠 동안 작성한 코드와 관련된 경우가 빈번하기 때문에 기억에 많이 남아 있고 빠르게 수정할 수 있다.
"스프린트"는 없다
2주 또는 3주의 스프린트는 미니 폭포수이다. 따라서 외부 이해관계자들에게 약간의 예측성을 제공하기 위한 의도를 위해 상당한 유연성을 희생한다. 하지만 당신이 고객 지향의 제품에서 작업한다면 사용자들은 당신이 어느 시점에 제품 업데이트를 시작할지에 대한 아무런 기대가 없다 (심지어 외부 이해관계자들도 그렇다. 내 생각에는 예측성이라는 것은 과대평가 되었다. 그것은 그냥 영업직 직원이 과장 판매를 회피하는 수단이다.)
과업의 연속적인 흐름은 같은 날에 버전1, 버전2, 버전3을 출시할 수 있고 버전1의 사용자로부터 배운 것을 버전2에 포함하며 버전2 사용자의 피드백을 바탕으로 버전3을 출시하는 것을 의미한다.
소규모 과제
이상한 얘기 같지만, 랜덤 행렬 이론의 흥미로운 결과를 볼 때 고차원 공간에서 극소점은 희귀하다 (이유는 도함수가 0인 대부분의 지점이 실제로는 안장점이기 때문이다). 나는 소프트웨어 엔지니어링이 대부분 매우 고차원적인 세상에서 발생하는데, 이런 세상에서는 과업을 작게 나누고 증분 방식으로 각각을 개별로 출시해서 언덕을 오르는 것이 가치를 제공하는 가장 빠른 방법이라 생각한다.
반대로 소프트웨어 엔지니어링에서 가장 무서운 일 중 하나는 출시 없이 쌓여 가는 코드의 "재고"이다. 이것은 배포의 위험을 나타냄과 동시에 누군가 사용자가 원하지 않는 소프트웨어를 만들고 있다는 위험 신호이기도 하다. 빠르게 기능 조각을 출시하지 못해 생기는 사용자 가치 손실은 말할 필요도 없다 (사용자 가치는 시간이 지나면서 통합이 필요한 기능 가치로 생각해야지 최종 상태에서의 기능 가치로 봐서는 안 된다).
기능 제한 (기능 선별 활성화)는 마지막 선택 사항이다. 우리는 이것을 드물게 사용한다. 심한 경우 기능 분기를 선택하기도 한다. 그것은 악마의 작업물이고 폐기되어야 한다. Git-flow는 끔찍한 발명품이다. 우리는 그것을 Spotify에 시도해 보았을 때 사람들은 본인 시간의 대략 50%를 코드 재작성에 소비했다.
오래된 풀 리퀘스트는 이런 이유로 기피된다. 이상적이라면 풀 리퀘스트는 몇 시간 이내에 머지되어야 하며 최대 수 백라인을 넘지 않아야 한다. 우리는 풀 리퀘스트에 리뷰어를 할당하고 슬랙 채널로 알리기 위해 고유한 시스템을 만들었다. 그 결과는 아래 그림처럼 명확하다. 이것은 우리의 monorepo의 데이터에서 가져온 것으로, 하나의 PR이 (Pull Request) 생성된 시점에서 머지된 시점까지의 시간을 나타낸다.
* 모노레포: 버전 관리 시스템에서 여러 프로젝트의 코드가 동일한 저장소에 저장되는 소프트웨어 개발 전략 |
풀 리퀘스트 생성에서 머지까지
복합 기능 팀과 팀원들
어떤 회사는 백엔드와 프론트엔드 팀을 분리한다. 또는 심한 경우 "머신 러닝 생산팀"과 떨어진 다른 도시에 "머신 러닝 이론 팀"을 유지하는 회사와 이야기를 나눈 적이 있다. 이렇게 하지 말아라. 이렇게 되면 이터레이션 속도가 느려지고 조정을 위한 오버헤드가 추가된다.
만일 빠른 피드백 루프를 목적으로 최적화하기를 원하면 직능으로 분리된 팀보다 복합 기능 팀이 훨씬 더 합리적이다.
이것은 개별 엔지니어에게도 잘 적용된다. 베터 닷컴의 모든 엔지니어는 백로그에 있는 어떤 기능도 가져갈 수 있고 출시할 수 있는 풀 스택 엔지니어이다. 대부분의 경우 복잡성은 백엔드에 존재하기 때문에 우리 팀의 대다수는 백엔드 개발자들로 치우쳐 있있다. 하지만 어느 누구도 CSS를 작성하거나 필요할 때 픽셀을 미는 것에 대해 이슈가 없다. 일반적인 과업의 80~90%는 백엔드이고 10~20%는 프론트엔드이다. 하나의 엔지니어가 하나의 기능을 담당하기 때문에 출시가 매우 빨라졌다. 팀 내의 대부분의 엔지니어가 CSS의 마스터는 아니다. 하지만 그들은 그 작업을 해서 일을 완료할 수 있다. 대체로 이런 작업은 과업을 출시하는 일의 큰 부분도 아니다.
고객 지향의 빠르게 움직이는 스타트업에서는 당신이 전문성을 기를 여유가 없을 것이다. 풀 스택 엔지니어는 더 빠르게 이터레이션을 수행할 뿐만 아니라 더 많은 유연성을 가지고 있다. 따라서 다음 주에 그 팀이 어느 스택에서 시간을 보낼지 알 수 없다.
내가 교조주의 적인 그림을 그리기 전에 우리 소수의 특화된 역할을 고용했다는 것 말하고 싶다. 우리는 테스트 자동화 엔지니어, 운영 인력, 소수의 프론트엔드 엔지니어를 채용했다. 우리는 특정한 영역에 약간 많은 "전문가"가 필요했다. 심지어 이런 엔지니어들도 해당 영역의 작업에서 시간이 오래 걸렸고 여전히 전체 스택을 옮겨 다니면서 시간을 소모했다.
그 밖에 다른 것은?
결과적으로 큰 차이를 만드는 아주 작은 것의 긴 꼬리가 (long tail) 존재한다.
나는 이에 대해 온종일 작성할 수 있다. 대신 왜 주기 시간이 (cycle time) 그토록 중요한지에 대한 몇 가지 메모로 정리하고 싶다.
반복이냐 죽음이냐
첫째 빠른 이터레이션 속도에 대한 최적화가 산출량의 최적화와 같은 것은 아니라는 걸 지적하고 싶다. 리틀의 법칙에 따르면 산출량이 λ일 때 이터레이션 속도는 W의 역수이다. λ와 W 사이의 관계는 복잡하다. 하지만 슬프게도 그것에 대한 좋은 기록은 찾을 수가 없다. 구글 검색을 해보면 다른 검색 결과 중에 fabtime.com의 기고를 찾을 수 있다. (* 현재 해당 링크는 깨져 있음)
다양한 변동성 수준에 대한 주기 시간 대비 산출량
이 차트를 살펴보면 이론적인 처리량을 약간만 낮추어 산출량을 줄이면 주기 시간을 크게 낮출 수 있다는 걸 알 수 있다 (말하자면 높은 이터레이션 속도). 하지만 칩 제조사들은 이야기할 만한 배움 프로세스가 없는 거대 규모의 제조 프로세스이다. 당신이 높은 산출량 정점에서 뭔가를 빠르게 배우고자 한다면 이론적인 처리량보다 약간 낮게 운영하는 게 당연하다.
다소 이론적인 이야기라 미안하지만, 다시 정리해 보자. 당신이 시간당 천 개의 햄버거를 만들어야 하는 패스트 푸드 체인점이라 가정하자. 어느 시점에 빵도 굽고, 패티도 그릴에 올리고, 양상추도 썰기 시작해야 한다. 모든 것이 거대한 배치 작업이고 (huge batches) 사전에 계획할 수 있다. 몇몇 소프트웨어 프로젝트도 이와 유사하다. 예를 들어 C++에서 자바로 전환하는 큰 애플리케이션을 재작성하는 경우이다.
하지만 대부분의 경우 소프트웨어 프로젝트는 완전히 새로운 햄버거 레시피를 찾으려고 노력하는 것과 같다. 이런 상황에서 배치 작업을 작게 유지하고 피드백을 통해 계속 배우는 것이 핵심이다. 당신은 한 시간에 500개 또는 심지어 800개의 햄버거를 만들 수 있고 배치 크기와 주기 시간을 10배나 더 작게 만들 수 있다. 당신의 재고를 (inventory) 계속 낮게 유지하라고 강제하는 것은 린 생산 방식을 관통하는 하나의 요체이다. 그리고 대개 이렇게 되면 당신은 더 빠르게 고객의 요구에 반응할 수 있게 된다 (또 다른 이유는 1950년대 일본에서 그런 재고가 실질적인 비용이었기 때문이다. 하지만 이건 이 기고의 주제가 아니다).
어쨌든 조직의 관점에서 한 사람은 양상추를 썰고 한 사람은 빵을 만들기보다 사람들에게 햄버거를 만들 책임이 지운다면 당신은 재고를 낮은 수준으로 유지할 수 있다. 또 피드백 루프를 짧게 만들면 양념 조합을 계속 변경할 수 있게 되어 당신이 얻은 피드백에서 배울 수 있다. 이렇게 해서 당신의 레시피는 10배 또는 100배 빠르게 진화할 것이다. 이것이 당신이 다른 모든 사람을 능가할 수 있는 최후의 방법이다.
EOD.
댓글 영역