Software Engineering at Google #3. Process - 스타일 가이드와 규칙

이전 글

8장 스타일 가이드와 규칙

이전 글에서 Part 1. 전제를 읽고 리뷰하였다. Part 2. 문화를 문화를 건너 뛰고 Part 3. 프로세스 부분을 바로 보자. 프로세스 부분에서는 구글의 스타일 가이드와 규칙, 코드 리뷰, 테스트, 폐기에 대한 내용을 다루고 있다. 이번 글에서는 그 중 8장 스타일 가이드와 규칙 부분을 살펴보자.

스타일 가이드란?

대부분의 엔지니어링 조직에서는 코드 베이스를 관리하는 규칙이 존재한다. 구글은 코딩할 때 따라야 하는, 혹은 하지 말아야 할 규칙들을 모아서 프로그래밍 스타일 가이드(Programming Style Guide)로 정리해 표준으로 삼았다. 이 스타일 가이드는 단순한 코드의 형식(포맷팅) 이상을 다룬다. 구글의 코드를 지배하는 종합적인 규약을 집대성한 것이 스타일 가이드이다. 구글은 이 스타일 가이드를 지배적으로 받들면서도, 엄격하게 가이드를 구성하지는 않는다. 가이드 라인을 제시하되 프로그래머가 스스로의 판단에 의해 해석하고 행동할 수 있도록 제한을 풀어놓는다.

스타일 가이드의 목적은 비슷하다. 코드의 지속 가능성을 높일 것. 이 목표에는 가독성 높이기, 일관된 코드 포맷팅과 규칙, 안전한 코드 작성 등이 포함된다.

한편 구글은 언어별로 코딩 스타일을 관리하는데, 각 언어별 스타일 가이드는 모두 공개되어 있고, 누구나 들어가서 볼 수 있다.

그 외에도 https://google.github.io/styleguide/ 이 링크에서 구글의 다양한 언어별 스타일 가이드와 규칙을 확인할 수 있다.

이때 흥미로운 점은, C++, Python, Java와 같은 몇몇 언어는 엄격하고 긴 스타일 가이드를 제시하는 한편 Go, C# 같은 언어는 외부 세계에서 통용되는 몇몇 규칙에 몇 가지 새 규칙만 추가해서 간단하게 적용한다는 것이다.

그리고 대부분의 코드는 외부 세계에서 통용되는 규칙을 따르는데, 또 몇몇 규칙은 그렇지 않다. 예컨대, 구글의 C++ 스타일 가이드는 외부에서 자주 사용되는 exception(예외 처리)를 허용하지 않는다. 이에 관한 글은 C++ 예외 처리(Error Handling) 가이드에서 다루었으며, 구글에서 오류 코드를 반환하는 방식인 absl::Status에 대해서는 C++에서 오류 코드를 반환해보자 이 글에서 다루었다.

규칙이 필요한 이유

확립된 규칙과 지침은 조직이 커지더라도, 일관되게 통용되는 공통의 코딩 어휘가 되어준다. 이렇게 되면 엔지니어들은 코드의 '형식'보다 코드에 담을 '내용'에 집중할 수 있게 된다. 또한 규칙은 장기적으로 좋은 코드를 작성하게끔 유도하므로, 엔지니어들 역시 무의식적으로도 '좋은' 코드를 작성하는 경향을 생기게 한다.

하지만 스타일 가이드 역시 트레이드오프이다. 개발 환경의 복잡도를 관리하고 엔지니어들의 생산성을 헤치지 않는 선에서, 코드베이스를 관리 가능하게끔 유지해야 한다. 이 목적을 달성하기 위한 규칙들 대다수가 엔지니어들의 자유를 제한한다. 유연성의 제한으로 인해 다소 불쾌해하는 엔지니어들이 있을 수도 있지만, 장기적으로 볼때 권위있는 표준은 일관성을 높이고 의견 대립을 줄여주므로 감수할 만한 트레이드오프이다.

구글의 스타일 가이드의 원칙

구글은 3만 명이 넘는 엔지니어들이 매일 약 6만 건의 코드를, 수십 년 동안 존재하게 될 20억 라인 이상의 코드 베이스에 서브밋한다. 구글의 스타일 가이드는 '규모와 시간 양쪽 측면에서 탄력적인 엔지니어링 환경이 지속되도록 하는 것'을 목표로 한다.
구글의 스타일 가이드는 기본적으로 다음 다섯 개의 원칙을 가진다.

  • 규칙의 양을 최소화할 것
  • 코드를 읽는 사람에게 맞출 것
  • 일관될 것
  • 오류가 나기 쉽거나 예상치 못한 동작을 유발하는 구조를 피할 것
  • 꼭 필요하다면 실용성을 생각해 예외를 허용할 것

규칙의 양을 최소화한다

스타일 가이드를 작성하고 적용할 때 주의해야 할 것은, 모든 것을 전부 다 스타일 가이드에 구겨넣지 않아야 한다는 점이다. 스타일 가이드가 방대하고 복잡할 수록, 조직 내 엔지니어들이 이 스타일 가이드를 익히고 적응하는데 더 큰 비용과 시간이 들게 될 것이며, 새로운 엔지니어들이 조직에 합류하기도 어렵게 된다. 그리고 스타일 가이드 역시 관리되어야 하는 문서인만큼, 이 양이 커질수록 이에 동반되는 관리 비용 역시 증가한다.

구글에서는 스타일 가이드가 법전처럼 해석되길 원하지 않는다고 설명한다. 무언가를 불허한다고 명시하지 않았다고 해서 이를 허용한다는 의미로 받아들이지 말아야 한다. 즉 너무나 자명한 규칙들은 굳이 작성하지 아니한다.

예를 들어서, 구글의 C++ 스타일 가이드는 goto 문에 대한 규칙이 없다. 이미 C++ 프로그래머들은 goto 문을 금기시하기 때문에, 굳이 이를 명시적으로 작성하지 않는다.

즉 한 두 사람의 예외적 상황을 가정하여 (예를 들어서 goto 문을 남발하는 엔지니어가 조직에 있을 것이라 가정하고) 매번 모든 상황을 명시적으로 기술하는 것은 전체 조직에게 정신적 부담을 주게 되고, 이는 조직 규모의 확장을 방해한다.

읽는 사람에게 맞춘다

한 가지 기억해야 할 점은, 코드는 쓰이는 횟수보다 읽히는 횟수가 훨씬 더 많다는 것이다. 그렇기 때문에 코드의 스타일 가이드는 쓰는 사람에게 맞추는 것이 아니라 읽는 사람에게 맞추어야 한다.

예를 들어서, Python의 조건부 표현은 if 문을 사용하는 것보다 짧아서 쓰는 사람에게 편리하지만, 읽는 사람에겐 다소 난해하여 가독성을 헤칠 수 있다. 그래서 구글에서는 Python에서 조건부 표현을 사용하는 것을 금지한다. 즉 구글에서는 '쓰기에 간편함'보다 '읽기에 간단함' 쪽에 가치를 둔다. 일종의 트레이드오프이다.

그렇기 때문에 변수와 타입 이름 역시 길게 서술적으로 작성할 것을 명시한다. 코드를 작성하는 사람에게는 타이핑 부담이 늘어나지만, 장기적으로 더 읽기 편한 코드를 만들어내기 위함이다.

또한 엔지니어가 의도한 행위를 분명하게 알려주는 증거를 남기라고 요구한다. 즉 이 코드가 무엇을 하려고 하는 지 읽는 사람이 즉시 알 수 있어야 한다.

예를 들어, Java, JS, C++의 스타일 가이드에서 슈퍼 클래스의 메소드를 오버라이드할 때 반드시 오버라이드하는 메소드에는 오버라이드 어노테이션이나 키워드를 사용하도록 요구한다. 설계 의도를 읽는 사람이 하여금 직접 파악하게 하지 않고, 명시적으로 표기해서 읽는 사람의 부담을 줄인다.

이러한 조건은 특히 행위와 의도에 오해의 소지가 있는 상황을 줄여준다. 특히 C++에서는, 코드의 일부만 봐서는 포인터의 소유권이 누구에게 있는지 파악하기가 어려울 때가 많다. 이 포인터가 호출된 콜러 쪽에 있는지, 함수에게 이전된 것인지, 내가 이 포인터를 직접 할당 해제해야 하는지 아니면 어딘가에서 계속 쓰이는지, 혹은 포인터를 넘긴 후에 함수 내에서 이 포인터를 삭제하였는지 아니면 계속 내가 사용할 수 있는지 등, 엔지니어들은 익숙하지 않은 함수를 마주했을 때 혼란에 빠진다.

이 문제를 해결하기 위해 구글에서는 소유권을 넘길 목적이면 std::unique_ptr을 사용할 것을 강제한다 (Ownership and Smart Pointers 참고) . 이렇게 함으로써 호출자는 소유권을 이전함을 명시적으로 기술해야 하고, 이는 읽는 이로 하여금 소유권 파악의 부담을 줄여준다. 아래 두 코드를 비교해보자.

/// @brief Foo*를 받는 함수, 전달받은 포인터의 소유권이 누구에게 있는지 알 수 없다.
void TakeFoo(Foo* arg);

// ...

/// 함수 호출이 끝난 후, 소유권을 누가 갖고 있을까? 알 수 없다.
Foo* my_foo(NewFoo());
TakeFoo(my_foo);

// 읽는 이: "대체 `Foo*`를 사용할 수 있는 거야 없는거야..."

반대로 다음 코드를 보자.

void TakeFoo(std::unique_ptr<Foo> arg);

// ...

// 함수 호출 시, 소유권이 이전됨을 명시하고, 읽는 이에게 소유권이 이동되었음을 알려준다.
std::unique_ptr<Foo> my_foo(FooFactory());
TakeFoo(std::move(my_foo));

// 읽는 이는 곧바로 `unique_ptr`을 사용할 수 없음을 알 수 있다.

위의 예시는 다시 말해서, 읽는 이가 직접 코드의 동작 방식에 대해 알지 못하더라도, API만 보고서도 상호 방식 자체를 추론할 수 있게끔 정보를 녹여낸다는 의미이다. 구글에선 이를 현 위치에서 추론하기라고 부른다. 즉 함수의 구현부를 들여다보지 않아도 충분히 호출 지점에서 무슨 일이 벌어지는 지 명확히 이해할 수 있도록 기술한다는 의미이다.

이에 근거해 주석 역시도, 읽는 이를 위해 적소에 증거를 남긴다. 문서화 주석(파일, 클래스, 함수 앞에 추가되는 코드 블럭)은 코드의 설계와 의도를 설명한다. 구현 주석(코드 자체 곳곳에 산재된 주석들)은 뻔하지 않은 선택을 한 이유를 설명하거나 주의할 점을 알려주고, 혹은 로직이 까다로울 경우 이에 대해 설명하고, 중요한 부분을 강조한다. Comments 이 글에서 구글의 주석에 관련된 가이드라인을 확인할 수 있다.

일관되어야 한다

구글은 모든 것을 일관되게끔 한다. 구글은 전 세계에 지부와 연구소가 흩어져 있는 다국적 기업임에도 불구하고 의도적으로 모든 곳의 시스템을 동일하게 관리한다. 구글 직원의 출입 카드는 전 세계의 구글 회사의 리더기에 읽히고, 회의실의 화상회의 인터페이스 역시 동일하다. 새로운 환경에 마주할 때마다 새롭게 설정을 익히는데 시간을 허비하는 비용을 줄인 것이다. 마찬가지의 노력을 코드에도 기울인다. 코드의 규칙을 일관되게 관리하고, 엔지니어들 역시 코드를 읽을 때 일관되어 있을 것이라 가정하고, 이를 확인하는데 드는 시간과 비용을 줄인다.

로컬 프로젝트라면 어느 정도의 개성이 허용되겠지만, 모든 도구, 기법, 라이브러리는 모두 일관되어야 한다.

한편 이 일관성은 구글 내부에서 통용되는 규칙이 아니라, 구글 외부에서 통용되는 규칙과의 연결성도 고려해야 한다. 책에서는 그 예시로 Python의 스타일 가이드를 설명한다.

공백 개수

구글에서는 파이썬의 공백 개수를 2칸으로 설정했었다. 이는 초창기 구글이 파이썬을 사용했던 이유는 순수한 파이썬 어플리케이션을 개발하려는 의도가 아니라 다른 C++ 프로젝트를 지원하려는 의도였고, 그래서 C++ 코드와의 일관성을 유지하기 위해 공백을 2개로 제한했던 것이다.

그러나 시간이 지나자 이 논리는 더 이상 합리적이지 않게 되었다. Python을 작성하는 엔지니어들은 점차 C++ 코드보단 Python의 코드를 더 많이 읽게 되는 일이 많아졌다. 그러면서 외부 코드를 참고할 때마다 이질감을 느끼게 되었다. 외부에서 통용되는 Python의 규칙은 공백 개수를 4칸으로 규정하고 있었기 때문이다. Python 엔지니어들은 외부 코드를 참고할 때마다 이질감으로 인해 생산성이 저하되었고, 이를 내부 프로젝트로 갖고 오는 과정에서도 코드를 다듬느라 시간을 허비하였다. 반면 내부 코드를 오픈 소스로 공개할 때에도 역시 외부에서 통용되는 표준에 맞추어 코드를 다듬는데 똑같은 노력을 기울어야 했다. Python만의 스타일 가이드가 필요해진 시점이었고, 결국 바깥 세상과의 일관성을 선택하기로 했다.

일반적으로 규칙을 만들 때는 최대한 바깥에서 통용되는 규칙과 일치하게끔 유지하는 게 유리하다.

오류를 내기 쉽거나 예상과 다르게 동작할 여지가 있는 구조는 피할 것

구글의 Python 스타일 가이드에서는 리플렉션 등의 몇가지 고급 기능을 사용하지 못하도록 제한한다. 예를 들어 다음 코드를 보자.

if hasattr(obj, 'foo'):
    return getattr(obj, 'foo')

그리고 다음 상황을 생각해보자.

# constant_file.py

A_CONSTANT = [
    'foo', 
    'bar',
    'baz',
]
# use_constant.py
values = []
for field in constant_file.A_CONSTANT:
    values.append(getattr(obj, field))

위에서 'use_constant.py' 파일만 보고서 foo, bar, baz 라는 필드에 접근하고 있음을 쉽게 알 수 있을까? 코드에서는 명확한 증거를 찾을 수 없다.

만약에 필드를 로컬에 있는 A_CONSTANT가 아닌, RPC나 다른 Data Storage에서 가져왔다면 어땠을까? 이처럼 읽기 어려운 코드는 단순히 메시지 확인만 잘못해도 눈치채기 어려운 중대한 결함을 만들어낼 수 있고, 또 이런 상황은 테스트하기에도 검증하는 것도 어렵다.

실용적인 측면을 인정하자

그럼에도 불구하고, 구글의 스타일 가이드 역시 실용적인 효과가 큰 경우에 예외를 허용한다. 성능 역시 고려해야 한다. 예를 들어서 구글의 C++ 스타일 가이드는 exception handling을 허용하지 않는다고 말했다. 그러나 컴파일러 최적화에 영향을 주는 noexcept 키워드를 적절한 상황에서 사용하는 것을 허용한다.

상호운용성 역시 중요한데, 예를 들어서 구글 C++ 스타일 가이드에서는 기본 명명 규칙은 카멜 케이스(CamelCase)이다. 그러나 STL 등 표준 라이브러리 기능을 모방하는 경우에는 스네이크 케이스(snake_case)를 허용한다. 또한 윈도우 플랫폼 기능을 활용하려면 어쩔 수 없이 다중 구현 상속도 허용한다 (근데 이건 애초에 MFC 등 윈도우 플랫폼을 사용할 때 일부 기능들이 어쩔 수 없이 다중 상속을 사용하게끔 만들어져 있기 때문인 것도 있다. 일반적인 경우에 다중 상속은 객체 간 의존성과 코드의 복잡성을 크게 증가시켜서, 매우 조심해야 되는 설계이다.).

또한 Java와 JS 스타일 가이드에서, 빌드 과정에서 자동으로 생성된 코드는 스타일 가이드이 규칙을 적용받지 않는다고 명시해놓는다. 이는 프로젝트의 통제권 밖에 있는 외부 컴포넌트에 자주 마주하거나 의존하기 때문이다. 즉, 일관성은 매우 중요하지만 그것이 융통성을 없앨 정도는 아니라는 얘기다.

스타일 가이드의 내용

스타일 가이드는 크게 세 범주로 나눌 수 있다.

  • 위험을 회피하기 위한 규칙
  • 모범 사례를 적용하기 위한 규칙
  • 일관성을 보장하기 위한 규칙

위험 회피하기

구글의 스타일 가이드에는 기술적인 이유로 반드시 써야 하거나 반대로 쓰면 안 되는 언어 특성에 관한 규칙들을 명시해놓는다. static variable & static member, lambda, exception handling, thread, class inheritance 등의 사용법을 설명하는 규칙을 담아놓으며, 어떤 기능을 사용하고 어떤 기능은 제한해야 하는 지를 설명한다.

모범 사례 강제하기

구글의 스타일 가이드는 모범 사례를 강제한다. 주석을 어디에 작성할 지부터, 작성자의 의도가 명확히 드러나지 않을 때 주석으로 남길 것 역시 강제한다. switch 문에서의 [[fallthrough]], 빈 catch 블록, TMP(템플릿 메타 프로그래밍) 등이 이에 해당한다. 더 나아가서 이름 규칙들, 소스 파일의 구조도 규칙, 코드 포맷팅에 관한 규칙 등을 기술한다.

한편 널리 이해되지 못하고, 새로 도입된 기능을 제한하기도 한다. 모범 사례가 충분히 누적되고, 엔지니어들 전반이 그 기능에 대해서 이해하기 전까지는 선제적으로 방어선을 구축하는 것이다. 예를 들어서, 구글에서는 std::unique_ptr을 초기에는 사용하지 못하게 했다. std::unique_ptr의 동작 방식, 소유권의 이동, move sementic 역시 새로운 개념이여서 많은 엔지니어들이 혼란스러워했기 때문이다.

시간이 흐르며 객체의 소유권을 명확히 알려주어 읽는 이에게 도움이 되는 std::unique_ptr의 효과가 입증되기 시작했고, move sementic의 작동 방식 역시 서서히 적응해나가기 시작했다. 비록 새로운 스마트 포인터의 복잡성, 무브 시멘틱은 여전히 우려 대상이었지만 비용을 치르더라도 std::unique_ptr을 도입하였다.

일관성 구축하기

구글의 스타일 가이드는 자명한 규칙은 없앰으로써 규칙의 양을 최소화하는 한편, 사소한 문제를 다루는 규칙도 많다. 이는 모두 일관성을 유지하기 위함이고, 엔지니어들이 더이상 형식에 의존하거나 고민하지 않고 내용에만 집중하도록 유도한다.

예를 들어서 명명 규칙, 들여쓰기 공백 수, import/include 문의 순서를 명시적으로 기술하여 엔지니어들이 이에 대해 고민할 시간을 줄인다.

규칙의 수정

위에서 Python의 공백 수에 대한 예시에서 알 수 있듯이, 구글의 스타일 가이드 역시 끊임없이 변화하고 진화하고 수정된다. 그리고 구글은 왜 이 규칙이 만들어지게 되었는지, 무엇을 위한 것인지 그 논리와 컨텍스트를 문서화하여 제공한다. 이를 기반으로 논리에 허점이 있음을 규칙 수정의 근거로 삼거나 규칙을 보호하는데 사용한다.

사례: 카멜 케이스 명명법

Python 스타일 가이드를 처음 제정할 때 구글은 Python을 C++의 래퍼로 사용하기 위한 목적을 갖고 있었고, 그에 따라 C++의 명명 규칙에 따라 함수명을 카멜 케이스(CamelCase)로 사용하기로 결정했다.

그러나 대부분의 파이썬 커뮤니티, 그리고 Python 공식 스타일 가이드라인 PEP 8에서는 함수명을 스네이크 케이스(snake_case)로 사용한다.

구글에서 외부 파이썬 프로젝트와 연동되는 일이 많아지고, 자연스레 카멜 케이스와 스네이크 케이스가 혼재되는 경우가 많아졌다. 또한 구글의 내부 파이썬 프로젝트를 오픈소스로 공개할 때도, 외부 커뮤니티와 이를 관리하는데 혼선이 빚어졌고, 이로 인한 비용이 올라갔다.

결국 이 문제가 논의 대상에 올라갔고, 비용과 이점을 고려한 결과 파이썬 스타일 가이드는 수정되었다. 즉 스네이크 케이스를 허용하는 대신, 한 파일 내에는 일관성을 유지할 것이 추가되어, 프로젝트 별로 적합한 방식을 택할 수 있게 되었다.

지침

구글에서는 규칙으로 받아들여지는 스타일 가이드 외에 다양한 형태의 지침도 관리한다. 여기에는 조언, 모범 사례에 대한 소개 등을 담고 있다.

  • 올바르게 구현하기 어려운 주제에 관한 언어별 조언(예: 동시성과 해싱)
  • 언어의 최신 버전에서 소개된 새로운 기능의 상세 설명과 구글 코드베이스에 적용하는 방법에 관한 조언
  • 구글 라이브러리가 제공하는 중요한 추상 개념과 데이터 구조 목록. 이미 만들어둔 구조를 새로 만드는 일을 방지해주고, '필요한 게 있는데 우리 라이브러리에서 이걸 뭐라고 부르는지 모르겠어' 식의 질문에 답해준다

책에서는 지침에 관한 예시로 금주의 팁이라는 조언서 시리지를 배포하였고, 성공적인 반응을 얻어냈다고 소개했다.

규칙 적용하기

규칙의 적용 역시 중요하다. 구글에서는 규칙을 적용하기 위해서, 최대한 자동화된 방법을 사용한다. 자동화된 방법은 코드 포맷터나 오류 검사기 등을 활용한다.

코드 포맷터로는 구글에서는 C++에는 clang-format, Python에는 YAPF를 래핑한 도구, Go에는 gofmt, Dart에는 Dartfmt, BUILD 파일에는 buildifier를 사용한다고 설명한다.

오류 검사기의 예시로는 clang-tidy(C++)나 Error Prone(Java) 등을 활용한다. clang-tidy에 관한 글은 이 글에서 다루었으니 참고하기 바란다.

그러나 도구를 이용한 자동화가 불가능한 검사 영역도 있다. 예를 들어서 '변경되는 코드의 크기를 작게 하라'라는 규칙에서 '작다'라는 기준을 명확히 정의하는 것은 어렵다. 가령 똑같은 한 라인을 파일 수백 개에서 수정하는 일은 실제로는 검토하기 쉬운 변경일 수 있다. 그러나 단 몇 줄의 코드만 수정하더라도 그에 동반되는 부수 효과가 많다면, 이를 작다라고 평가하긴 어려울 것이다.

이렇듯 기술적인 문제를 다루는 규칙이라면 가능한 한 기술적으로 자동 집행되게끔 만들되, 주관적인 방법의 경우는 엔지니어들의 재량에 맡겨야 한다.

마무리

Chapter 8은 구글의 스타일 가이드와 규칙에 관한 구글의 설명을 기술하는 챕터였다. 마지막으로 읽어볼 만한 몇 가지 글들을 소개한다.

참고 자료

  • 타이터스 윈터스. (2022). 구글 엔지니어는 이렇게 일한다 (pp. 205-231). n.p.: 한빛미디어.
  • Google C++ Style Guide