구글 엔지니어는 이렇게 일한다: 소프트웨어 엔지니어링


프로그래밍과 소프트웨어 엔지니어링의 가장 큰 차이는 시간, 규모(확장), 트레이드 오프이다.

엔지니어는 시간의 흐름과 언젠가 변경될 가능성에 신경써야 한다.
조직은 만들어 낼 소프트웨어 자체 뿐 아니라 제작하는 조직까지 양 측면 모두에서의 확장과 효율에 더 집중해야 한다.
엔지니어는 수명과 성장 속도를 정밀하게 예측하기 어려운 상황에서 결과에 더 큰 영향을 주는 보다 복잡한 결정을 내려야 한다.

소프트웨어 엔지니어링에서 프로그래밍은 결국 소프트웨어를 제작하는 수단일 뿐이다.
여기서는 프로그래밍 작업(= 개발, development) 와 소프트웨어 엔지니어링 작업(= 개발 + 수정 + 유지보수) 의 차이에 대해 알아본다.

시간이 프로그램에 미치는 영향은 ‘이 코드의 예상 수명은?’ 에 대해 생각해보면 된다.
모든 가치있는 변경에 대응할 수 있다면 ‘그 프로젝트는 지속 가능하다’ 라고 말한다.
여기서 중요한 것은 역량만을 따진다는 것이다. 가치가 충분하지 않거나 더 중요한 일을 하기 위해 변경하지 않기로 할 수도 있다.

규모의 관점으로는 ‘몇 명이 참여하는지?’, ‘엔지니어들이 개발과 유지보수의 어느 부분에 관여하는지?’ 에 대해 생각해보면 된다.
조직이 성장하고 프로젝트가 확장될수록 생산 효율도 높아지는지, 개발 워크플로의 효율도 성장에 발맞춰 개선되는지는 규모와 관련이 깊다.
프로젝트나 조직 규모가 확장되면서 발생하는 문제는 흔히 정책에 영향을 주며, ‘반복 수행하는 일들에 비용을 얼마나 쓸 것인지?’ 와 같은 소프트웨어 지속 가능성을 묻는 질문에 답하는 기초가 된다.

소프트웨어 엔지니어링은 의사 결정의 복잡성과 이해관계 측면에서도 프로그래밍과 차이가 난다.
소프트웨어 엔지니어링은 주기적으로 여러 선택지 사이의 트레이드 오프를 평가해야 한다.
소프트웨어 엔지니어나 리더는 지속 가능성을 잃지 않으면서 조직, 제품, 개발 워크플로의 규모를 확장하는 비용을 관리해야 하며, 트레이드오프를 평가하여 합리적인 결정을 내려야 한다.
만일 유지보수에 도움이 되는 변경을 연기하거나 확장성이 떨어지는 정책을 받아들여야 하는 상황이라면 다음에 다시 검토해야 할 수도 있음을 잊지 말아야 하며, 이 결정 때문에 생긴 지연 비용을 정확히 계산해두어야 한다.



1. 시간과 변경

현업에서도 단명하는 코드를 다루는 개발자를 볼 수 있다.
초기 단계의 스타트업에서 일하는 엔지니어들은 투자 비용이 회수되기 시작할 때까지 회사가 살아남지 못할 가능성이 크기 때문에 장기적인 투자보다는 바로 눈 앞의 목표에 집중하는 경향이 있다. 그래서 계속 초기 스타트업들에서만 일해왔다면 10년차 개발자라고 할지라도 소프트웨어를 1~2년 이상 유지보수해본 경험이 전무할 수 있다.

기대 수명과 업그레이드의 중요성에 대해 알아보자.
기대 수명이 길어질수록 업그레이드를 해야할 필요성이 있는데 (대부분의 프로젝트는 5년 이내에 업그레이드를 함) 이 시점부터 프로젝트는 외부 환경의 변화에 대비를 시작해야 한다.
초기부터 업그레이드를 계획하지 않은 프로젝트라면 해당 프로젝트에서 수행해본 적 없는 작업을 진행해야 하고, 일반적인 업그레이드보다 작업 규모가 큰 경우가 많기 때문에 이 시점이 매우 힘들어진다.

수명이 길어질수록 ‘동작한다’ 와 ‘유지보수 가능하다’ 의 차이를 더 분명히 인지해야 한다.


1.1. 하이럼의 법칙(Hyrum’s law)

‘동작한다’ 와 ‘유지보수 가능하다’ 의 차이를 구분 짓는 중요한 요인 중 하나는 하이럼의 법칙이다.
하이럼의 법칙은 암시적 의존성 법칙(The Law of Implicit Dependencies) 라고도 한다.

  • 하이럼의 법칙
    • API 사용자가 충분히 않다면 API 명세에 적인 내용은 중요하지 않음
    • 시스템에서 눈에 보이는 모든 행위(동작)을 누군하는 이용하게 될 것이기 때문임

하이럼의 법칙은 개념적으로 엔트로피와 유사하다.
시간의 흐름에 따른 변경과 유지보수를 논하려면 하이럼의 법칙을 알아야 한다.

엔트로피(Entropy)

시스템의 불확실성, 무질서, 정보량을 측정하는데 사용됨
(분야에 따라 의미와 해석이 약간씩 달라질 수 있음)

어떤 시스템의 혼란스러운 정도를 설명할 때 사용되기도 하는데 예를 들어 정돈된 방은 엔트로피가 낮다고 표현함

본질적으로 엔트로피는 무질서 혹은 불확실성의 측정치로, 시스템이 얼마나 예측 불가능하도 무작위적인지 나타냄

엔트로피가 낮아지지 않는다고 해서 효율을 포기해서는 안되듯이, 소프트웨어 유지보수에 하이럼의 법칙이 적용된다고 해서 계획 세우기나 효율성을 포기해서는 안된다.

API 인터페이스를 명확하게 작성해놓아도 현실에서는 API 사용자가 명세에 없는 기능을 찾아 활용하기도 하며, 그 기능이 널리 쓰이면 추후 API 를 변경하기 어렵게 된다. API 의 노출 시간이 길어지고 사용자가 늘어나면 가장 무해할 듯한 변경도 일부 사용자의 소프트웨어를 망가뜨릴 수 있다.

무해할 듯한 변경이 일부 사용자의 소프트웨어를 망가뜨리는 예시
(출처: 워크플로)

  • 스페이스바를 누르면 CPU 가 과열되는 현상 제거
    • 오래된 사용자1
      • 사용자의 컨트롤 키가 고장나서 대신 스페이스바를 이용하고 있었음
      • 온도가 급작스럽게 상승하면 스페이스바가 컨트롤 키로 인식되도록 설정을 변경하여 사용하고 있었음
      • 따라서 이번 업데이트로 인해 정상 이용이 불가하니 롤백해달라고 요청

1.2. ‘변하기 않기’를 목표로 하지 않는 이유

시간과 변경에 대처해야 하는 이유의 밑바탕에는 ‘변경을 피할 수 없다’ 라는 가정이 깔려있다.

아무것도 변하지 않을 것이라 가정하여 하트블리드(Heartbleed) 패치를 적용하지 않거나, 멜트다운(Meltdown) 과 스펙터(Spectre) 과 같은 추측 실행 문제를 완화해두지 않으면 매우 위험하다.

하트블리드 취약점

하트비트 확장 기능 구현 문제로 발생한 결함으로 데이터가 노출되고, TLS/SSL 통신 보안을 무너뜨릴 수 있었음
하트블리트 패치는 이런 취약점을 수정한 OpenSSL 버전(1.0.1g)을 배포하면서 위 문제를 해결하기 위한 업데이트임

OpenSSL 1.0.1g 이후 버전에서 문제가 해결되었으며, 최신 버전으로 업데이트하는 것이 필수적이었음 하트블리드 사건 이후 코드 리뷰와 오픈소스 프로젝트의 보안 감사, 지속적인 최신 보안 업데이트가 더욱 중요해졌음

멜트다운(Meltdown) 과 스펙터(Spectre)

멜트다운은 CPU 의 Out-of-Order Execution 메커니즘을 악용하여 메모리 보호 경계를 우회할 수 있는 취약점으로 해커가 운영체제 커널 메모리에 접근할 수 있는 취약점임

스펙터는 멜트다운과 달리 특정 메모리 보호 경계를 우회하기보다 다른 프로세스가 사용하는 메모리 데이터를 추출하는 취약점임

멜트다운과 스펙터는 CPU 설계의 기본적인 약점을 악용하여 민감한 데이터를 노출하는 하드웨어 문제로써 운영제체 및 브라우저 업데이트를 통해 어느 정도 방어됨

효율 개선은 미묘한 문제점이 있다.

예를 들어 오래 전에 만들어 둔 알고리즘이 있는데 이 데이터 구조가 최신 장비에서 효율이 떨어질 수도 있다.
(CPU 클록과 메모리 지연시간의 격차가 점점 벌어지면서 ‘효율적인’ 코드의 모습이 변함)

따라서 소프트웨어 설계도 제때 변경해주지 않으면 최신 하드웨어를 도입하는 효과가 퇴색된다.

기존 설계가 완벽히 논리적이고 합리적인 모범 사례를 충실히 반영한 것일 수도 있으므로, 적절한 변경이 뒷받침되어야 새로운 선택지의 도입 효과가 극대화된다.
이처럼 이전 시스템에 문제가 없더라도 시간이 흐르면 변경을 진행할 이유가 자연스럽게 만들어지기도 한다.


2. 규모 확장과 효율성

코드베이스 자체도 확장 가능해야 한다.

코드가 많아지고 변경 이력이 쌓이는 등의 이유로 빌드 시스템이나 버전 관리 시스템이 점점 느려진다면 어느 순간 더는 정상 운영할 수 없는 시점이 온다.
‘전체 빌드에 걸리는 시간’, ‘리포지터리에서 전체를 새로 내려받는 시간’, ‘프로그래밍 언어 버전을 업그레이드하는 비용’ 같은 지표는 적극적으로 관리하지 않으면 천천히 악화된다.
이런 문제들은 조직 차원에서 챙기며, 확장 가능성에 신경써야만 안정되게 관리할 수 있다.


2.1. 확장하기 어려운 정책들

시스템 폐기를 예로 들어보자.

폐기에 대한 좀 더 상세한 내용은 추후 다룰 예정입니다. (p. 52)

기존에 사용하던 위젯을 버리고 새로운 위젯을 개발한다고 하자.

이런 방식은 의존성 그래프가 조금만 깊고 넓어지면 바로 실패하고 만다. 단 한 번의 빌드 실패가 영향을 미치는 범위도 함께 늘어나는데 이런 문제를 ‘확장 가능한 방식으로 푼다’ 라고 한다면 폐기를 처리하는 방식을 바꾼다는 뜻이다.
마이그레이션 작업을 사용자가 아닌 시스템 담당 팀 내부에서 스스로 처리하도록 하는 것이다.
그렇게 함으로써 노하우를 축적한 하나의 팀이 모두 처리하므로 규모의 경제 효과도 누릴 수 있다.

인프라팀은 사내 사용자들이 새 버전으로 옮기는 것을 돕거나 직접 업데이트하되, 하위 호환성을 유지해야 한다.
마이그레이션을 사용자가 알아서 대응하면 인프라 변경에 영향을 받는 모든 팀에서 사태를 파악하고, 문제를 해결하고, 더 이상 쓸모없어진 것들은 폐기해야 한다.

개발 브랜치도 확장성 문제의 좋은 예이다.
독립된 개발 브랜치가 완료되려면 트렁크(메인 개발 브랜치)로 머지되고, 테스트되어야 하므로 아직 다른 개발 브랜치에서 작업 중인 엔지니어들은 다시 동기화하고 테스트하느라 시간을 허비하게 된다.

규모가 커진다면 다른 방식이 필요하다.

다른 방식에 대해서는 추후 다룰 예정입니다. (p. 54)


2.2. 확장 가능한 정책들

구글에는 ‘인프라를 변경하여 서비스가 중단되는 등의 문제가 발생하더라도, 같은 문제가 지속적 통합(CI)의 자동 테스트에서 발견되지 않는다는 인프라팀의 책임이 아니다.’ 라는 정책이 있다.
이 정책을 비욘세 규칙이라고 하는데 ‘네가 좋아했다면 CI 테스트를 준비해뒀어야지’ 라는 뜻이다.
즉, 공통 CI 시스템에 추가해두지 않은 테스트는 인프라팀이 책임지지 않는다는 뜻이다.

비욘세의 Single Ladies 의 가사 중 ‘네가 좋아했다면 반지를 끼워줬어야지’ 에서 가져온 말임

공유 포럼은 조직 확장에 큰 기여를 한다.
100명의 엔지니어가 있는 조직에서 질문에 답해줄 전문가가 1명만 있어도 곧 더 나은 코드를 작성하는 100명의 엔지니어가 생겨난다.


2.3. 사례: 컴파일러 업그레이드

하위 호환성이 좋다고 알려진 경우라도 언어와 컴파일러 업그레이드는 섬세하고 어려운 과제이다.
컴파일러가 업그레이드될 때는 거의 항상 어딘가 미묘하게 다르게 동작하는 곳이 있기 마련이다.

만일 앞선 5년 동안 컴파일러 업그레이드를 한 번도 하지 않았다면 컴파일러 업그레이드 작업은 결국 컴파일러와 언어의 변경 사항 중 적용할 방법을 찾지 못한 것들의 우회법과 단순화 트릭을 찾는 일로 변질된다.
또한, 코드베이스에서는 크고 작은 하이럼의 법칙 문제들이 나와서 특정 컴파일러 버전에 더 깊이 의존하게 만들수도 있다.

CI 시스템이 적용되지 않으면 컴파일러 변경이 초래할 영향을 사전에 알기 어렵고, 회귀 문제를 일으키지 않으리란 보장도 할 수 없다.

회귀 문제

제대로 작동하던 기능이 오작동하는 문제
일반적으로 기능 추가, 리팩터링, 버그 수정 등 다른 목적으로 코드를 수정하는 과정에서 뜻하지않게 일어남

이러한 문제를 극복하려면 자동화(한 사람이 더 많은 일을 수행), 통합과 일관성(저수준 변경이 영향을 미치는 범위 제한), 전문성(적은 인원으로 더 많은 일을 수행) 가 되어야 한다.

인프라는 자주 변경할수록 오히려 변경하기가 쉬워진다.
각 요소의 코드가 수정을 여러 차례 거치고 나면 하부 구현의 미묘한 차이에 의존하는 일이 없어지고, 대신 언어나 OS 차원에서 보장하는 추상 개념을 활용하도록 변경된다.

<코드베이스의 유연성에 영향을 주는 요인들>

  • 규칙적으로 릴리스하여 릴리스 사이의 변경량을 줄임
  • 규칙적인 업그레이드
  • 업그레이드를 정기적으로 수행함으로 인해 그 과정에서 중복되는 작업을 찾아 자동화
  • 비욘세 규칙과 같은 유용한 정책과 절차를 갖춤
    • 이러한 정책 덕분에 인프라팀은 미지의 사용 방법까지 걱정할 필요없이 업그레이드 진행 가능 (CI 시스템에 반영된 사용법만 고민)

2.4. 원점 회귀(왼쪽으로 옮기기)

개발 과정에서 문제를 일찍 발견할수록 비용은 적게 든다.

개발의 순서는 보통 아래와 같다.
개념잡기 → 설계 → 구현 → 리뷰 → 테스트 → 커밋 → 카나리 → 배포

카나리(Canary)

변경 사항을 소수의 사용자 집단에만 배포하여 검증하는 테스트
베타 테스트와 달리 사용자에게 서비스가 변경되었음을 통지하지 않고 진행함

위 과정에서 문제 발견 시점을 왼쪽으로 이동시킬수록 수정 비용이 줄어든다.
이렇게 왼쪽으로 옮기는 행위를 원점 회귀라고 한다.
만일 배포한 후에 취약점이 발견되면 해결하는데 큰 비용이 든다.

코두 커밋 전에 정적 검사나 코드 리뷰로 찾아낸 버그를 배포 이후에 발견한 버그보다 훨씬 쉽게 고칠 수 있다.
그래서 개발 프로세스 초기에 품질, 안전성, 보안 문제를 찾아 알려주는 도구와 관례를 제공하는 일은 매우 중요하다.


3. 트레이드오프와 비용

비용은 금액만을 지칭하는 것이 아니다.

  • 금융 비용: 돈
  • 리소스 비용: CPU 시간
  • 인적 비용: 엔지니어링 노력
  • 거래 비용: 조치를 취하는 비용
  • 기회 비용: 조치를 취하지 않는 비용
  • 사회적 비용: 선택이 사회 전체에 미치는 영향

위에 나열한 비용 외에도 현상 유지 편향(= 현재 상태를 유지하려는 경향)과 손실 회피와 같은 부분도 고려해야 한다.

결국 엔지니어링 조직의 선택을 결정지는 요인은 아래로 압축된다.

  • 반드시 해야하는 일(법적 요구 사항, 고객 요구 사항)
  • 근거에 기반하여 당시 내릴 수 있는 최선의 선택(적절한 결정권자가 확정)

3.1. 의사결정을 위한 근거 자료

근거 자료의 가중치를 정하는 시나리오는 주로 아래 2가지이다.

  • 관련한 정량적 데이터를 모두 측정할 수 있거나 최소한 추정이라도 할 수 있는 경우
    • 예) CPU 와 네트워크, 금액과 메모리 양 사이의 트레이드오프를 평가할 때 등
    • 예) 2주 동안 특정 기능을 고성능 기능으로 변환한다면 시스템의 메모리를 5GB 더 사용하지만 필요한 CPU 수를 2,000개 줄일 수 있다. 진행해야 할까?
      이 물음에 답하려면 메모리가 CPU 의 상대적 비용은 물론, 인건비(소프트웨어 엔지니어가 2주간 작업)와 기회 비용(2주 동안 엔지니어가 할 수 있는 다른 일)까지 고려해야 함
  • 측정하기 어렵거나 측정 방법을 모르는 정성적 데이터
    • 예) 엉망으로 설계된 API 의 엔지니어링 비용을 측정할 방법은?
    • 이것은 쉬운 답이 나오기 어렵우므로 절충안을 찾기 위해 경험, 리더십, 선례에 기대게 됨
    • 정량화하기 어려운 정보를 정량화하는데 도움이 되는 것에 대한 내용은 추후 다룰 예정입니다. (p. 61)


3.2. 사례: 시간과 규모 확장 사이에서 결정

만일 우리 팀에만 해당하는 문제가 생겼는데, 해결하려면 의존성을 추가하는 것이 좋을지 아니면 fork 하거나 다시 구현하는게 좋을 지 선택해야 하는 경우가 있다.

당면한 문제에만 최적화한 해법이 모든 가능성에 대비하는 범용적인 해법보다 성능이 뛰어날 가능성이 크기 때문에 코드를 fork 하거나, 다시 구현하여 주어진 문제만을 해결하도록 하면 더 확실하게 최적화할 수 있다. 또한 외부 솔루션에 의존하지 않고 온전히 내가 통제할 수 있다.

한편, 모든 개발자가 기존 솔루션을 재활용하는 대신 전부 fork 해버린다면 확장성과 지속 가능성이 위협을 받는다.
외부 라이브러리를 사용한다면 보안 문제가 발생했을 때 그 라이브러리만 수정한 후 사용자들이 수정된 라이브러리를 이용하도록 알려주기만 하면 되는데 모두 fork 를 받아서 사용중이라면 라이브러리의 fork 들 중 결함이 적용되는 것들을 모두 찾아내고 해당 fork 들을 이용하는 사용자도 모두 추적해야 한다.


4. 소프트웨어 엔지니어링 vs 프로그래밍

소프트웨어 엔지니어링이 프로그래밍보다 우수하다는 것이 아니다.
단지 이 둘에 적용되는 제약 사항, 가치, 모범 사례가 다르다는 것이다.

단 며칠만 활용할 프로젝트에 통합 테스트나 지속적 배포(CD) 등을 적용할 필요는 없다.

소프트웨어 엔지니어링과 프로그래밍 차이의 대부분은 시간 흐름에 따른 코드 관리, 시간 흐름에 따른 규모 확장의 영향, 이런 관점에서의 의사결정 방식에 있다.
프로그래밍은 코드를 생상하는 즉각적인 행위이고, 소프트웨어 엔지니어링은 활용 가치가 남아있는 한 오랫동안 코드를 유용하게 관리하고, 팀 간 협업을 가능하게 하는 정책, 관례, 도구 모두를 아우르는 종합적인 개념이다.


참고 사이트 & 함께 보면 좋은 사이트

본 포스트는 구글 엔지니어는 이렇게 일한다 를 읽으며 정리한 내용들입니다.






© 2020.08. by assu10

Powered by assu10