개발 중에는 모든 오류 경로를 즉시 처리하기보다 경고를 활용해 Rust의 작업 흐름을 더 유연하게 만드는 기법과 `wip` 크레이트를 소개합니다.
Rust는 정확성을 우선시하는 신뢰할 수 있는 언어입니다. 하지만 개발 중에는 모든 단일 오류 경로를 즉시 고려해야 하는 일이 부담이 될 수 있습니다.
이 글에서는 더 오래 happy path에 머무를 수 있도록 정확성 처리를 뒤로 미루는 몇 가지 기법과, 이 과정을 더 편리하게 만들기 위해 제가 개발한 라이브러리를 소개합니다.
🌱 이 글은 다소 서둘러 쓰였을지언정 생성형 AI 도구를 사용하지 않고 100% 손수 작성되었습니다. 오타와 어색한 문장을 그 증거로 받아들여 주세요.
복잡한 로직을 작업하는 Rust 개발자에게 rustc가 갑자기 방해가 되는 것보다 더 나쁜 일이 있을까요? 아직 Result를 반환하지 않았거나, 어떤 오류 경우를 처리하지 않았거나, 저 경계 사례를 구현하지 않았기 때문이든, 컴파일 오류의 원인은 아주 많고 지금 달성하려는 목표에서 주의를 빼앗을 수 있습니다.
이런 일이 생기면 보통 두 가지 선택지가 있습니다:
TODO later 같은 표시를 대충 붙여 경고로 낮춘 뒤, 일단 작업을 계속 진행한다.이 글에서는 나중에 처리할 즉시 오류를 경고로 낮추는, 즉 두 번째 선택지를 Rust에서 구현하는 몇 가지 기법을 다룹니다.
경고는 컴파일 상호작용의 가능한 범위에서 딱 알맞은 위치를 차지합니다:
오류는 다른 컴파일 단계(라이프타임 등)와 테스트를 막아서 일반적인 워크플로를 방해합니다:
어떤 오류든 (1)에서 (2)로 넘어가는 것을 막습니다.
반대로 컴파일 메시지를 완전히 억누르면, 문제를 잊어버리고 그대로 프로덕션까지 흘려보낼 위험이 있습니다 😬
경고는 개발을 막지 않지만, PR을 머지하기 전에 반드시 정리해야 합니다.
Meilisearch에서는 CI에서 컴파일러를 -D warning 플래그와 함께 실행합니다. 그러면 모든 경고가 CI 시점에는 오류로 바뀌어, 실수로 WIP 코드를 배포하지 않도록 보장할 수 있습니다.
물론, 읽습니다. 코드 읽기(내 코드와 동료의 코드 모두)는 버그를 막는 훌륭한 장벽이고, 코드 리뷰는 팀 내에서 지식을 효과적으로 공유하기 위한 필수 조건입니다.
이 글에서 다루는 모든 기법은 전문적인 규모에서 코드를 작성할 때 정확성을 유도하고 더 강한 가드레일을 만드는 추가적인 수단입니다. 품질을 높여 주지만, 기본적인 좋은 실천을 대체하지는 않습니다.
소프트웨어 엔지니어로 일한 제 경험상, 사람이 절대 실수하지 않는 것에 의존하는 일은 결국 실패하게 되어 있습니다.
서론은 이쯤 하고, 바닐라 Rust에서 오류 지연 전략을 구현하는 데 쓸 수 있는 몇 가지 기법을 소개하겠습니다.
unwrap를 곳곳에 쓰기Rust가 오류를 올바르게 다룰 구조를 제공해 준다는 점은 중요하지만, 그렇다고 우리가 그 오류를 즉시 다뤄야 한다는 뜻은 아닙니다.
오류 처리를 미루는 흔한 패턴은 Result나 Option을 그냥 unwrap해서 내부 값을 꺼내는 것입니다. 그 대가는 오류 경로에서 패닉이 난다는 점입니다.
그런 다음 나머지 코드가 준비되면 변경한 부분으로 돌아와 임시 조치로 넣어 둔 unwrap들을 선택적으로 제거하고, 적절한 오류 처리로 바꿀 수 있습니다.
fn does_not_return_result_yet(some_params: SomeType) -> SomeReturnType {
let converted = some_params.try_into().unwrap() // <- 실패 가능한 변환, 일단 무시
converted.try_foo().unwrap() // <- 실패 가능한 연산, 일단 무시
}
clone를 곳곳에 쓰기소유권 문제도 비슷한 부류이지만, 오류 처리와는 약간 다르고 훨씬 덜 흔합니다. 소유권을 제대로 유지하려면 때로는 설계를 수정해야 하며, 그렇지 않으면 라이프타임 문제가 나타날 수 있습니다. 이런 경우 자주 쓰는 완화책은 값을 이리저리 이동시키는 대신 복제하는 것입니다.
이 범주의 문제는 수정 방법이 코드 구조를 만만치 않게 바꾸는 일을 포함할 수 있기 때문에, 수정을 뒤로 미루는 것이 조금 더 까다롭습니다. 그래도 경험이 쌓이면, 적절한 변수를 스택 상위로 전달함으로써 나중에 clone을 제거할 수 있을 사례를 초기에 알아볼 수 있습니다.
todo! 매크로Rust 표준 라이브러리는 todo!() 매크로를 제공하며, 그 목적은 다음과 같이 설명됩니다:
미완성 코드를 나타냅니다.
프로토타이핑 중이어서 코드가 타입 분석은 통과하도록 둘 자리표시자가 필요할 때 유용할 수 있습니다.
이는 실제로 인터페이스의 형태를 잡고 함수 정의를 사용할 수 있게 만들어야 하지만, 아직 동작하는 구현을 제공할 필요는 없을 때 매우 유용합니다.
todo!는 diverge합니다(패닉을 일으킵니다). 따라서 컴파일러는 대부분의 타입에서 이를 자리표시자 표현식으로 사용할 수 있고, 그래서 문서에서 타입 분석을 언급하는 것입니다. todo를 널리 사용하면 일부 코드 경로(오류뿐만 아니라)를 나중에 구현하도록 미뤄 둘 수 있습니다.
unimplemented!()와 혼동하지 말 것저는 적어도 한 번은 누군가가 todo!()에 대해 인터넷에서 틀린 말을 하는 것을 본 적이 있습니다. 그들은 이것이 프로그램 사용자에게 어떤 기능이 아직 구현되지 않았음을 알리기 위한 것이라고 말하곤 했습니다.
사실 Rust에는 그 용도를 위한 전용 매크로가 있으며, 이름은 unimplemented!()입니다.
todo!()는 대상이 다릅니다. 코드 작성자와 리뷰어를 위한 것입니다. 어떤 todo도 프로덕션에 도달해서는 안 되며, 이것은 오직 WIP Rust를 위한 편의 기능입니다.
mut, 사용되지 않는 변수 등컴파일러가 기본으로 제공하는 경고는 개발 중에 매우 유용합니다. 다음과 같은 것들을 잡아냅니다:
이런 것들은 대개 코드 개발이 아직 끝나지 않았을 때 나타납니다. 예를 들어 구현이 오로지 todo!()로만 이루어진 함수는 보통 매개변수에 대한 이런 경고를 띄웁니다.
⚠️개발 중에 이런 경고를 임시로 고치고 싶은 충동을 참는 것이 중요합니다. 바인딩 앞에 _를 붙이거나 다른 요령을 써서 말입니다. 경고를 인위적으로 없애면 미완성 코드를 그대로 넘겨버릴 위험이 있습니다. 경고를 무시하는 데도 쓸 자리는 있지만, 이 경우는 아닙니다!
코딩하다 보면 경계 사례나 불완전하거나 깨진 로직처럼, 나중에 수정하거나 처리해야 할 무언가가 떠오르는 일이 자주 있습니다. 명시적인 오류 처리와 달리 이런 종류의 문제는 타입 시스템에서 추적되지 않기 때문에, 현재 구현 중인 로직에서 주의가 흐트러지길 원치 않으면 보통 그 문제를 나중에 고치라는 메모 주석을 남깁니다.
이 자체로는 괜찮지만, 나중에 정말로 그 자리로 돌아가야 한다는 걸 제가 기억해야 합니다 😅
이를 위한 수단으로, 저는 종종 그런 주석 앞에 문서 주석인 것처럼 슬래시 세 개를 붙이는 요령을 씁니다.
Rust는 아무것도 문서화하지 않는 문서 주석에 대해 경고를 내기 때문에, 어떤 문장 위에 문서 주석을 달면 일반적으로 그 주석이 설명하는 문제와 연결된 경고를 생성하는 목표를 이룰 수 있습니다.
이런 기법들은 수년 동안 제게 큰 도움이 되었습니다(이제 Rust를 쓴 지도 10년이 넘었습니다...). 하지만 저에게도 불만은 있습니다.
대체로 가장 성가신 문제는, 이런 기법들 중 일부는 코드를 프로덕션에 보내기 전에 해결해야 할 문제와 연결된 경고를 안정적으로 생성하지 않는다는 점입니다.
unwrap를 제거하는 것을 잊고 릴리스 이후에 그것이 불타오르는 모습을 보는 일은 엔지니어로서 느낄 수 있는 최악의 감정 중 하나입니다 😓
todo!는 이 범주에서 자주 등장하는 묘한 문제아입니다. 앞서 말했듯 종종 경고를 만들기는 하지만, 이는 간접적이며 예를 들어 함수 매개변수가 사용되지 않는다거나, 일어나야 할 변경이 일어나지 않는다는 식의 이유 때문입니다. 그래서 조금 교활해질 수 있습니다. todo!를 쓰면 흔히 경고가 뜬다는 데 익숙해지고, 경고가 없다는 사실에 의존해 머지 버튼을 눌러 버리기 쉽기 때문입니다.
unwrap와 clone은 더 심합니다. 이들은 경고를 전혀 내지 않으므로, 최종 PR 전에 이를 제거하기 위해 다시 읽을 때 훨씬 더 주의해야 합니다.
추가적인 문제는 이들 중 일부가 원래 프로덕션까지 가도록 의도된 것이라는 점입니다. 이 글에서 설명한 "과감하게 unwrap하기" 전략에서는 unwrap의 정상적인 사용 사례와 그것의 "WIP" 사용 사례를 뒤섞게 됩니다.
마지막으로, "TODO 문서 문자열"은 일종의 해킹입니다. 어쩌면 행복한 작은 사고라고 말할 수도 있겠네요. 하지만 이런 성격 때문에 직관적이지 않은 실패 방식이 있습니다. 현재 문서 주석은 표현식에는 금지되어 있어 컴파일 오류를 일으키기 때문입니다. 자주 일어나는 일은 아니지만, ///를 실제로 무언가를 문서화하는 위치에 써 버릴 위험도 있습니다. 그건 배포하기에도 썩 좋지 않고, 기대한 경고도 나오지 않습니다.
wip 크레이트 소개WIP Rust를 작성할 때 겪는 이런 자잘한 고통들에 대응하기 위해, 저는 "나중을 위한 경고" 전략을 더 쉽게 구현할 수 있도록 wip라는 크레이트를 개발하기로 했습니다.
대체로 이것은 Rust를 개발하는 동안 어떤 맥락에서든 사용할 수 있는 도구 모음으로 이루어져 있습니다.
wip 사용하기다른 어떤 크레이트와 마찬가지로 wip를 의존성에 추가하면 됩니다:
cargo add wip
선택적으로, wip를 사용하고 싶은 파일들에서는 wip가 제공하는 prelude를 가져올 수 있습니다:
use wip::prelude::*;
wip는 Rust의 사용 중단 경고에 의존하므로, 에디터에서 deprecated 함수 호출에 취소선을 긋는 기능은 끄는 것을 권합니다. 그렇지 않으면 시각적으로 너무 거슬리게 됩니다.
wip::wip!, 경고를 내는 todo!wip가 제공하는 첫 번째 도구는 wip! 매크로입니다. todo!와 정확히 동일하게 동작하지만, 사용할 때마다 항상 경고를 냅니다. 코드를 미완성으로 표시하기 위해 todo!를 쓸 만한 곳이라면 어디든 이것을 쓰면 됩니다.
todo!가 아닌가요?초기 버전의 wip는 todo라는 이름을 썼지만, 그렇게 하면 prelude를 사용할 수 없게 됩니다. 의존성이 std 매크로를 가리려 드는 것을 Rust가 좋아하지 않기 때문입니다...
unwrap_wip와 clone_wip이 메서드들은 Result와 Option에 대한 확장 트레이트로 구현되어 있습니다. 스코프 안에 들어오면 일반적인 unwrap와 clone과 정확히 똑같이 동작하지만, 사용할 때 경고를 냅니다.
또한 최종 코드에 이 unwrap/clone이 남아 있기를 의도한 것이 아니라는 점을 분명히 드러내 줍니다! 다만, 항상 그렇게 끝나는 것은 아니라는 점은 염두에 두세요...

wip::fixme!, 메모를 위한 패닉 없는 매크로제가 wip에서 가장 자주 쓰는 도구 중 하나는 fixme! 매크로입니다. 이것은 wip!의 패닉 없는 변형으로, 표현식을 대신하지 않습니다. TODO 주석과 같은 방식으로 동작하지만, 해킹 같은 부분은 없고 경고는 안정적으로 나옵니다.
저는 최근 몇 개의 PR에서 wip 크레이트를 사용해 왔고, 그중에는 제 터무니없이 큰 것들도 포함됩니다. 보통 개발 중에는 워크스페이스의 크레이트들에 wip 의존성을 추가해 두었다가, 리뷰 전에 히스토리를 정리하면서 제거합니다.
이 경험은 꽤 해방감이 있었습니다. 정확히 "이게 이렇게까지 필요할 줄은 몰랐다"는 범주에 들어갑니다. 나중에 잊지 않으면서도 happy path에 집중하기가 훨씬 더 쉬워집니다.

좋아하지 않을 이유가 있을까요?
제가 떠올릴 수 있는 단점은 다음뿐입니다:
fixme를 추가할 때는 충분한 맥락을 적어 두어야 합니다. 그렇지 않으면 아주 나중에 돌아왔을 때 고치기 어려워질 수 있습니다. 보통은 문제가 되지 않습니다. wip는 대개 (대체로) 짧은 PR 안에, 한 명의 작성자(또는 페어 프로그래밍한 둘)에게 한정되기 때문입니다.wip가 유발한 경고 수가 다소 기가 꺾이게 만들 수 있습니다. 특히 큰 PR에서는 더 그렇습니다. 아마 그런 PR은 피하는 게 좋겠네요 😅그럼에도 저는 앞으로도 wip를 계속 사용할 것이라고 꽤 확신합니다. 여러분은 개발 중에 경고를 어떻게 활용하시나요? 현재 API에 어떤 추가 사항이 유용할 것 같나요?