Rust = 안전, 정말 그게 끝일까
Rust를 한 번이라도 써본 분이라면 "메모리 안전성"이라는 말을 귀에 못이 박히게 들어보셨을 거예요. 널 포인터 역참조도 없고, 데이터 레이스도 컴파일 타임에 잡아주고, use-after-free도 원천 차단되니까요. 그래서 Rust를 도입하면 마법처럼 버그가 사라질 것 같은 환상을 갖기 쉬운데요. corrode.dev에서 올린 글은 그 환상에 살짝 찬물을 끼얹으면서, Rust가 잡지 못하는 버그들에 대해 솔직하게 이야기해요.
결론부터 말씀드리면, Rust는 "메모리 안전성"과 "데이터 레이스"라는 매우 좁은 영역의 버그를 잡는 데 탁월하지만, 우리가 실제로 프로덕션에서 마주하는 버그의 대부분은 그 영역 바깥에 있다는 거예요. 그래서 Rust를 쓴다고 해서 테스트나 코드 리뷰를 줄여도 되는 건 절대 아니라는 얘기죠.
어떤 버그들을 못 잡냐면요
글에서 언급하는 첫 번째는 로직 버그예요. 예를 들어 할인율을 계산하는 함수에서 price * discount를 써야 하는데 실수로 price + discount로 적었다고 해봐요. Rust 컴파일러는 두 코드 모두 타입이 맞으니까 아무 에러도 안 내요. 이건 Rust든 Go든 Java든 다 똑같이 못 잡는 버그죠. 그런데 신규 개발자들이 "Rust니까 괜찮을 거야"라는 안도감으로 테스트를 게을리하면 이런 버그가 그대로 배포돼요.
두 번째는 잘못된 수치 처리예요. Rust는 정수 오버플로우를 디버그 빌드에선 패닉으로 잡아주지만, 릴리즈 빌드에선 그냥 wrap around(한 바퀴 돌아서 음수가 됨)시켜요. 부동소수점 비교 같은 건 어떤 언어에서도 까다로운데, Rust도 마찬가지고요. 단위 변환 실수(미터를 피트로 바꾸지 않은 채 더하는 것 같은) 같은 것도 컴파일러는 알 길이 없어요.
세 번째가 흥미로운데, 데드락과 같은 동시성 버그예요. Rust는 데이터 레이스(여러 스레드가 같은 메모리를 동시에 만져서 망가지는 상황)는 컴파일 타임에 잡지만, 데드락(서로 락을 기다리며 멈춰버리는 상황)은 못 잡아요. Mutex를 두 개 잡는 순서가 스레드마다 다르면 그대로 데드락이 나는데, 컴파일러는 이걸 알아챌 방법이 없어요. 또 채널을 통한 메시지 전달에서도 디자인 실수로 영원히 기다리는 상황이 충분히 생길 수 있고요.
네 번째는 리소스 누수예요. 메모리는 RAII로 잘 정리되지만, 파일 핸들이나 데이터베이스 커넥션을 풀에 반환하지 않거나, 백그라운드 태스크를 cleanup하지 않으면 그대로 누수가 발생해요. Drop 트레이트를 잘못 구현하거나 빠뜨리면 더더욱 그렇고요.
그럼 Rust의 가치는 뭘까
그렇다고 Rust가 별 의미 없다는 게 아니에요. 글의 진짜 메시지는 "Rust를 쓴다면 그 보장이 어디까지인지 정확히 알고 쓰자"예요. Rust가 보장하는 건 메모리 안전성, 데이터 레이스 없음, 그리고 강력한 타입 시스템을 통한 일부 로직 표현력이에요. 이 영역에서 Rust는 정말 강력해서, C/C++로 짰으면 일주일을 디버깅했을 버그가 처음부터 컴파일이 안 되는 식으로 막혀요.
그리고 Rust의 enum과 패턴 매칭, Result/Option 같은 타입은 실제로 로직 버그도 상당수 줄여줘요. "이 케이스를 처리하지 않았다"는 걸 컴파일러가 강제로 알려주니까요. C에서 흔한 "에러 코드를 무시하고 진행"하는 실수가 Rust에선 거의 일어나지 않죠.
다른 언어들과 비교하면
Go는 단순하지만 nil 패닉이나 데이터 레이스에 약하고, Java는 GC 덕에 메모리는 안전하지만 NullPointerException이 여전히 살아 있어요. Haskell이나 OCaml 같은 함수형 언어는 타입 시스템으로 더 많은 걸 잡아내지만 학습 곡선이 가파르고, 시스템 프로그래밍엔 잘 안 쓰이고요. Rust는 "시스템 프로그래밍 영역에서 도달할 수 있는 안전성의 새로운 기준"을 만든 거지, "모든 버그를 없애는 마법"은 아닌 거죠.
한국 개발자에게 주는 시사점
요즘 한국에서도 백엔드나 임베디드, 블록체인, 게임 엔진 쪽에서 Rust 도입을 검토하는 팀이 부쩍 늘었어요. 도입을 추진하시는 분들은 사내 설득 자료에 "Rust로 바꾸면 버그가 X% 줄어듭니다"보다, "메모리 안전성 클래스의 버그를 거의 제로로 만들 수 있고, 그 외 영역은 여전히 테스트와 리뷰가 필요합니다"라고 정직하게 쓰는 게 장기적으로 신뢰를 얻는 길이에요.
실무에선 Rust 프로젝트라도 속성 기반 테스트(proptest, quickcheck), fuzzing(cargo-fuzz), 로직 단위 테스트를 충분히 깔아두는 걸 추천해요. 데드락 의심 구간엔 parking_lot의 deadlock detection이나, 락 순서를 강제하는 패턴(예: 항상 정해진 순서로만 락 획득)을 쓰는 것도 좋고요.
마무리하며
Rust는 강력하지만 만능이 아니에요. "컴파일러를 통과했다 = 버그가 없다"는 공식은 절대 성립하지 않아요. 하지만 "컴파일러를 통과했다 = 메모리 안전성과 데이터 레이스 영역의 버그가 없다"는 매우 가치 있는 보장이고, 이걸 정확히 이해하고 쓸 때 Rust의 진가가 나오는 것 같아요.
여러분은 Rust를 쓰면서 "이건 컴파일러가 잡아줄 줄 알았는데 안 잡아줘서 당황했던" 버그가 있으신가요? 어떤 종류였는지 공유해주시면 다른 분들에게도 큰 도움이 될 것 같아요.
🔗 출처: Hacker News
"비전공 직장인인데 반년 만에 수익 파이프라인을 여러 개 만들었습니다"
실제 수강생 후기- 비전공자도 6개월이면 첫 수익
- 20년 경력 개발자 직강
- 자동화 프로그램 + 소스코드 제공