요약
구글이 안드로이드·Chromium·Fuchsia 등 사내 Rust 코드에 공통으로 적용할 스타일 가이드를 공개했습니다.
구글 오픈소스 스타일 가이드 사이트에 올라온 문서입니다. 구글 사내 저장소와 안드로이드, ChromeOS, Fuchsia, Chrome의 Rust 코드를 쓰기 편한 쪽보다 읽고 유지하기 쉬운 쪽으로 맞추는 것이 목표라고 밝혔습니다.
왜 중요한가
- 공식 Rust 스타일 가이드와 API 가이드라인을 바탕으로, 큰 조직에서 쓸 규칙을 한곳에 모은 문서입니다.
- 스타일 논쟁을 줄이고 자동화와 대규모 일괄 수정을 쉽게 하려고, 개인 취향보다 일관성을 앞세웁니다.
- 라이브러리 코드와 애플리케이션 코드의 기준을 나눠, 앱 코드에서는 섣부른 일반화보다 단순함을 택하게 했습니다.
핵심 내용
- 공개 enum에는
#[non_exhaustive](나중에 항목이 늘 수 있음을 표시하는 속성)를 붙이라고 권했습니다. - unsafe는 안전한 대안이 없을 때만 쓰고, 작은 추상화로 감싼 뒤
SAFETY:주석으로 근거를 남기게 했습니다. - Rc·Arc(참조 횟수로 소유권을 나누는 포인터)는 되도록 피하고, 복제보다 빌림을 먼저 쓰라고 했습니다.
- 서식은 rustfmt에 맡기고, 개인 취향 때문에 자동 서식을 끄는 것은 금지했습니다.
- Chromium은 하위 폴더 없는 작은 크레이트(Rust의 컴파일 단위)를 선호하고, async와 표준 스레드를 쓰지 않습니다.
HN 반응
- 공개 enum에
#[non_exhaustive]를 붙이라는 권고에는, 새 항목을 컴파일 오류로 잡지 못한다는 반론과 API 버전 관리가 쉬워졌다는 경험담이 맞섰습니다. - 작은 크레이트 규칙에는 의문이 나왔지만, 크레이트 단위로 병렬 빌드와 재컴파일이 이뤄져 이점이 있다는 설명이 이어졌습니다.
읽을 때 가장 중요한 맥락입니다.
저는 러스트 1.0 시절부터 회사에서 러스트를 써 왔는데, 그러면서 깨달은 점이 있습니다.
온갖 프로젝트, 팀, 사람에게 "일괄적으로 똑같이 적용"하기에 가장 좋은 스타일 가이드는 여러 면에서 "내가 선호하는 스타일"과 같지 않습니다.
마찬가지로 이런 가이드가 매번 "최선"인 것도 아니지만, "의견이 갈리는 결정을 내릴 필요 없이 균일하게 적용할 수 있다"는 장점이 있습니다. 다시 말해 뚜렷하게 좋은 답이 없는 "스타일만의" 결정 때문에 리뷰나 코드 작성 중에 마찰이 생기지 않습니다.
좀 더 일반적인 예를 들어 보겠습니다. 관련된 로직이 "가까이 모여 있지" 않고 서로 다른 폴더의 작은 파일 3개 이상에 흩어지게 만드는 프레임워크나 스타일 가이드가 많습니다. 이건 엄청난 고통일 수 있고, 저라면 개인 프로젝트를 그렇게 관리하지 않을 겁니다. 하지만 코드를 가까이 두는 방식이 여러 이유로(주로 공유 로직이나 횡단 관심사 때문에) 통하지 않을 때가 있고, 그때는 코드를 어디에 어떻게 둘지 타협해야 하는데, 무조건 기계적으로 적용할 수 있는 "생각 없이 쓰는 만능 해법"은 없습니다. 이게 마찰로 이어질 수 있습니다. 같은 코드를 만지는 사람이 너무 많아지면 엉망진창이 되기 쉽기도 하고요. 그래서 회사들은 코드를 쪼개 놓는 스타일 가이드를 씁니다. 단순한 경우에는 결과물이 눈에 띄게 더 불편한데도 그렇습니다. 대규모 팀, 제각각인 실력, 같은 코드에서 일하는 많은 사람이라는 조건에서는 이쪽이 확장되기 때문입니다.
공개 enum에 #[non_exhaustive]를 적용하라는 건 말도 안 되는 권고 같아요. enum의 힘은 대부분 새 케이스를 추가했을 때 컴파일러가 고쳐야 할 패턴 매칭을 전부 짚어 준다는 데서 나오잖아요. 그래서 각 케이스를 하나하나 따져 보게 되니 좋은 건데요. 나머지를 다 받는 catch-all을 두면 개발 시간은 아끼겠지만, 새 케이스가 제대로 처리되지 않을 위험이 있어요. 컴파일 오류여야 할 게 런타임 문제가 되는 거죠.
규모가 커지면 분산 시스템을 다룰 때는 API 모델에 속하는 공개 enum에
#[non_exhaustive]를 기본값으로 쓰는 게 맞습니다. 필요하면 exhaustive enum을 쓰도록 선택할 수도 있어야 하지만, 실제로는 그런 경우가 훨씬 드뭅니다.저는 엔드포인트가 수천 개인 아주 큰 러스트 코드베이스를 가진 회사에서 일합니다. 저장소를 여러 개 쓰고, 각 서비스가 자기 API를 크레이트로 배포합니다. 몇 년 전에는 이 크레이트를 손으로 작성했고, 각 팀이 API에 exhaustive enum을 자주 썼습니다. 그래도 런타임 문제는 막지 못했습니다. 오래된 클라이언트가 새 서버로부터 새 변형(variant)이 담긴 응답을 받을 수 있었기 때문입니다. 버전 관리도 힘들어졌습니다. semver로는 메이저 버전을 올려야 하는데, 러스트는 명목적 타이핑(nominal typing)이라 메이저 버전이 다른 타입은 서로 다른 타입으로 취급됩니다. 그래서 이런 타입을 통합하기가(공유 헬퍼 함수, 확장 트레이트 등을 생각해 보세요) 몹시 불편했습니다. 결국 사람들은 새 변형을 기능 추가 버전(minor)에 넣어 배포했고, 중간에 낀 라이브러리의 컴파일을 깨뜨려 의존성 트리를 쑥대밭으로 만들었습니다.
그 뒤로 API 모델을 IDL에서 코드를 생성하는 방식으로 바꿨습니다. 제가 그 프로젝트를 이끌었고, 러스트 코드에는 non_exhaustive enum을 생성하기로 했습니다. 2년 전 일인데 상황이 극적으로 좋아졌습니다.
CI 단계에서는 다른 댓글에서 언급된
non_exhaustive_omitted_patterns린트를 돌립니다. 이런 공개 API에는 그 방식이 맞습니다. 큰 차이는 경고가 지금 작업 중인 크레이트에만 국한된다는 점이라, 오류보다 실제로 조치하기 쉽습니다.이건 모두 분산 시스템의 API를 모델링하는 맥락에서 한 이야기입니다. 지금은 평범한 헬퍼 라이브러리에서도 non_exhaustive를 더 자주 쓰는 편입니다. 코드의 순방향 호환성을 미리 계획해야 하니 항상 고민이 필요합니다.
흥미롭네요! 공유해 주셔서 감사합니다.
저도 동의합니다. 다른 분들 말씀에도 일리가 있긴 하지만요.
타입스크립트에서 (러스트도 마찬가지일 것 같은데) 제가 바라 온 건, 알려진 enum 값들은 빠짐없이 다뤘는지 타입 검사를 하면서 동시에 알 수 없는 값도 반드시 처리하게 강제하는 겁니다. 그런 유틸리티를 만들어 봤는데, switch/case 같은 네이티브 구문에 비하면 쓰기가 좀 어색합니다.
양날의 검입니다. #[non_exhaustive]가 없으면 변형을 추가하는 것이 semver상 메이저 버전을 올려야 하는 호환성 파괴 변경이 되니까요. 우선순위에 달린 문제입니다. 마이너 릴리스에서 변형이 늘어날 것으로 예상되는 enum(흔한 예가 오류 타입)과 닫힌 상태로 유지될 것으로 예상되는 enum(Option이나 SocketAddr 같은 것) 사이에는 미묘한 차이가 아마 있을 겁니다.
솔직히 제가 보기에 대부분의 enum은 오류 타입이나 설정 옵션(사용자가 보통 만들기만 하고 매칭은 하지 않는 것)을 빼면 아예 공개되지 않습니다. 이 두 경우에는 #[non_exhaustive]가 타당합니다. 공개되지 않는 enum에서는 컴파일러가 exhaustiveness를 강제해 주는 혜택을 그대로 누릴 수 있고요.
#[non_exhaustive]가 없으면 변형 추가는 언제나 semver상 호환성을 깨는 변경입니다.
#[non_exhaustive]가 있으면 변형 추가가 호환성을 깨지 않는 변경이 되긴 하는데, 그 변형을 무시해도 안전할 때만 그렇습니다. 무시할 수 없다면 여전히 호환성을 깨는 변경인데, 컴파일러 오류가 나지 않습니다. 제 생각엔 이게 아주 큰 단점입니다.
그러고 보니 PNG 형식이 이 문제를 다루는 방식이 떠오릅니다. PNG 파일은 서로 다른 종류의 청크(이미지 데이터, 색상 프로파일, 텍스트 주석 등)의 목록으로 이루어져 있습니다. 각 청크에는 플래그 비트가 있어서, 이미지를 디코딩할 때 무시해도 안전한지, 이미지 데이터가 바뀔 때 그대로 보존해도 안전한지 등을 알려 줍니다. 덕분에 디코더는 알 수 없는 청크라도 무시해도 되는 것은 건너뛰고, 무시하면 안 되는 것은 오류로 처리할 수 있습니다.
크로미엄 고유 지침 중에서 아주 작은 크레이트를 쓰라는 것, 특히 모듈이나 소스 파일을 중첩하지 말라는 부분을 누가 좀 설명해 주실 수 있나요?
함수는 가능한 한 작아야 한다던 2010년경 분위기가 떠오르네요. 그 영향으로 모든 함수가 한두 줄짜리인 코드를 리뷰한 적이 많은데, 불필요한 추상화 계층이 겹겹이 쌓여서 실제로 무슨 일이 일어나는지 더는 알 수 없었어요.
크롬에게 의심의 이익을 주고 싶긴 한데, 이건 정말 엉뚱해 보이네요.
Cargo로 빌드하는 러스트에서는 크레이트가 추상화 비용이 큽니다. 자체 디렉터리, Cargo.toml, src/ 하위 디렉터리 등이 필요하니까요. 제가 예전에 써 본 구글 빌드 시스템(크로미엄도 따르는 듯합니다)에서는 여러 언어의 소스 파일을 같은 디렉터리에 섞어 두고, 라이브러리를 필요에 맞게 더 잘게 나눕니다.
러스트에서 크레이트는 빌드 병렬화의 단위이니, 특히 인지적 부담이 크지 않다면 크레이트가 많을수록 이점이 있습니다. 일반적인 러스트 프로젝트에서 모듈을 따로 나눠 쓰듯이 크레이트를 쓴다고 생각하면 됩니다.
예시를 찾으려고 코드 검색을 뒤져 봤지만 러스트 코드는 별로 보이지 않았습니다. 아직 초기 단계인 것 같습니다. 그래도 https://source.chromium.org/chromium/chromium/src/+/main:testing/rust_gtest_interop/BUILD.gn 에는 별도 크레이트가 되는 러스트 파일이 4개쯤 있습니다. (그중 하나는 proc 매크로를 정의하니, Cargo로 빌드하는 러스트 프로젝트였더라도 별도 크레이트여야 했을 겁니다!)
경로로 임포트하는 매크로 기반 임포트가 있어서 크레이트 이름이 생성되는 것 같습니다.
https://source.chromium.org/chromium/chromium/src/+/main:testing/rust_gtest_interop/rust_gtest_interop.rs;l=6
컴파일 시간을 최적화하려는 것과 관련이 있을 수 있습니다. 크레이트가 컴파일 단위이므로 크레이트가 작으면 관련 없는 코드를 대량으로 다시 컴파일하게 될 가능성이 줄어듭니다.
크레이트를 작게 나누고 모듈 중첩을 피하면, 코딩 에이전트가 컨텍스트 윈도를 터뜨리지 않고 코드를 grep으로 찾기 쉽게 만드는 데도 도움이 되겠죠.
크레이트의 granularity가 어떤 추상화를 만드느냐에 영향을 줘서는 안 됩니다. 모듈과 타입은 필요한 만큼, 함수도 필요한 만큼 크게 만들 수 있습니다. 다만 하나의 큰 크레이트가 아니라 모듈을 여러 크레이트로 나눠 조직하라는 말입니다. 트레이트 일관성 규칙(coherence rules)도 언급하면서, 더 작은 크레이트를 추구하느라 어색한 설계를 강요하지 말고 라이브러리를 키우면 된다고 합니다.
작은 크레이트의 이점 하나는 링크와 의존성 그래프를 더 세밀하게 다룰 수 있다는 점이라고 생각합니다. 특히 재빌드에서 그렇습니다. 예전 직장에는 모든 걸 담은 "utilities" 크레이트가 있었는데, IP 주소 유틸리티만 쓰려고 의존했더니 무선 주파수 유틸리티까지 딸려 오는 문제가 생겼습니다. 이 서브모듈 중 하나가 업데이트되면 버전 변경이 과도하게 하위의 모든 소비자에게 전파됐습니다. "net-utils"와 "rf-utils"라는 작은 크레이트로 나뉘어 있었다면 문제가 덜했을지도 모릅니다.
이러면 사람들이 타입에 중복 정보를 넣게 됩니다(
io::Error대신IoError).HashMap처럼 널리 알려진 타입이 아니라면, 문맥상의 의미를 알 수 있게 전체 경로를 쓰는 쪽을 선호합니다.Source처럼 일반적인 이름을 다루고 있다면 그게 무엇을 뜻할까요? 코드를 이해하려고 임포트를 찾아봐야 해서는 안 됩니다(LSP가 있어야만 이해된다면 더욱 그렇고요).임포트를 줄이면 불필요한 머지 충돌도 줄어듭니다.
아래쪽에
io::Error같은 경우에는 "모호한 경우"이니 그렇게 써야 한다고 나와 있습니다.regex::Regex가 아니라 그냥Regex를 쓰라고 구체적으로 언급하는데, 제게는 그쪽이 더 합리적으로 보입니다.구글의 스타일 가이드는 네임스페이스 중복을 줄이는 경향이 있습니다.
자바에서 foo.widget.widgetBuilder.widgetFactory 같은 끔찍한 이름을 본 기억은 다들 있을 겁니다. 중복을 덜어 내고 신호 대 잡음비를 높게 유지하는 데 초점을 맞춘 명명 규칙이나 최소한 가이드라인이 있으면 좋죠.
아니요, 아름답잖아요! https://github.com/EnterpriseQualityCoding/FizzBuzzEnterpriseEdition
그 지침은 API 설계에 대응한 것입니다. 저는 기본 입장(타입을 임포트하라)이 API 설계에 어떻게 되먹임되는지를 말하는 겁니다.
(취미로) Objective-C/Cocoa를 쓰다가 나중에 Swift로 넘어가 봤는데, NSString은 못생겨 보여도 뭔가를 검색할 때는 String에 비하면 정말 훌륭하다고 말하지 않을 수 없습니다.
솔직히 어느 쪽이든 일괄 규칙은 나쁜 생각이라고 봅니다. 수식하는 게 맞을 때도 있고, 안 하는 게 더 명확할 때도 있으니까요.
구글은 저한테 빛이 많이 바랬어요. 이젠 구글이라는 브랜드가 다른 기술 회사들보다 한 수 위라는 의미를 갖던 2010년대가 아니잖아요.
요즘은 구글의 권고를 마이크로소프트나 IBM의 권고와 똑같은 정도로만 신경 써요.
여전히 큰 회사이고 똑똑한 사람도 많지만, 평균을 넘어서는 건 없어요.
요즘은 어떤 회사들을 우러러보시나요? 진짜 궁금해서 묻는 거예요.
제가 먼저 말해 볼게요.
Oxide Computer요.
그건 따로 Ask HN으로 올릴 만한 주제네요.
라이브러리 코드와 애플리케이션 코드를 구분해 둔 점이 정말 마음에 듭니다. 러스트는 복잡한 제네릭 코드 때문에 악평을 너무 많이 듣지만, 대부분의 애플리케이션 상황에서는 그 정도로 복잡하고 제네릭한 코드를 쓸 일이 거의 없습니다.
확장 트레이트에 관해 대부분의 러스트 사용자가 모르는 팁을 알려 드립니다. 트레이트 위에
#[rust_analyzer::prefer_underscore_import]를 붙이면(rust-analyzer를 쓰고 있고 RustRover가 아니라면), 자동 완성과 임포트 보조 기능이 그 트레이트를 기본적으로as _로 임포트해 줍니다. 확장 트레이트에 아주 유용합니다.좋네요. 저는 구글과 메타의 러스트 프로젝트를 눈여겨보면서, LLM에 시켜 스타일을 추려 내게 하고 그걸 따라 하며 익혀 왔습니다. fmt와 clippy 설정을 살펴보는 것도 도움이 되고요.
하지만 이 가이드처럼 프로젝트마다 일관되고 믿을 만하지는 않았습니다. 이번 건은 구글 잘했네요!
내용 대부분은 러스트 생태계 전반에서 아주 표준적인 것들이지만, 한곳에 모여 있으니 정말 도움이 됩니다.
이제 새로 C나 C++를 쓰는 사람이 있기는 한가요? 대기업에서조차 사양길에 접어든 언어 같은데요.
윈도, 리눅스, 크롬, gcc, LLVM, AAA 게임 엔진 같은 대형 소프트웨어 프로젝트 대부분이 지금도 새 C와 C++ 코드를 많이 쓰고 있을 겁니다. 임베디드 쪽은 거의 모두가 그렇고요.
혼자 개발할 때는 저는 여전히 C와 C++를 선호합니다. 오히려 작업 흐름에 LLM을 끌어들인 뒤로는 C와 그 단순함에 점점 끌리고 있습니다. 직접 손으로 쓰는 코드가 줄면 C++의 편의 기능 상당수는 그냥 필요가 없어지거든요.
LLM으로 C를 쓰는 건 좀 어이없게 느껴져요. (정말 C가 필요한 경우가 아니라면요.) LLM은 아직 불완전해서, 사람이 쓴 C처럼 UB 버그를 잔뜩 만나게 될 거예요.