LANGUAGE DOSSIER
Rust는 신뢰성과 성능을 동시에 요구하는 팀에게 점점 더 현실적인 선택이 되고 있다
핀테크 코어, 플랫폼 도구, 고신뢰 백엔드, 시스템 레벨 유틸리티. 장애 비용이 크고 데이터 무결성이 중요한 곳에서 존재감이 커진다.
러닝 커브는 있지만, 한 번 팀에 안착하면 “불안해서 손 못 대는 코드”를 줄이는 힘이 크다.
Negotiation
컴파일러가 거절하는 코드의 정체
다른 언어에서 넘어온 사람이 Rust에서 처음 겪는 일은 “문법은 알겠는데 빌드가 안 된다”입니다. 그리고 거절당한 코드는 대개 문법 오류가 아닙니다. 다른 언어라면 그대로 컴파일돼 돌아가다가, 운이 나쁜 날 특정 순서로 실행될 때만 터졌을 코드입니다. 해제된 메모리를 가리키는 참조, 순회 중에 늘어난 벡터, 락 없이 두 스레드가 만지는 값. Rust는 이런 코드를 실행 시점의 사고가 아니라 컴파일 오류로 바꿔 놓습니다.
그래서 Rust를 배우는 일은 문법 암기가 아니라 협상에 가깝습니다. 컴파일러는 “이 값의 수명이 저 참조보다 짧다”처럼 프로그램에 대한 주장을 하고, 우리는 코드 구조를 바꿔 그 주장이 성립하도록 만듭니다. 이 과정이 처음에는 방해로 느껴지지만, 지나고 나면 대부분 실제 설계 문제였습니다. 소유자가 누구인지, 언제 해제되는지, 동시에 접근하는 쪽이 있는지를 사람이 대충 넘겨 왔던 것뿐입니다.
대가는 정직하게 말하는 편이 좋습니다. 초기 개발 속도가 느리고, 빌드 시간이 길고, 팀이 익숙해지기까지 몇 달이 걸립니다. 그래서 Rust가 값을 하는 자리는 분명합니다. 장애 한 번의 비용이 큰 결제·정산 코어, 오래 떠 있어야 하는 데이터 처리 데몬, 메모리 사용이 예측 가능해야 하는 에이전트와 CLI, 다른 언어에 붙여 쓰는 고성능 라이브러리. 반대로 요구사항이 매주 바뀌는 초기 제품, 화면을 빨리 찍어 내야 하는 CRUD, 한 번 쓰고 버릴 스크립트에는 어울리지 않습니다.
Why It Works
이럴 때 특히 힘을 발합니다
소유권 규칙이 사고를 줄인다
해제 후 접근과 데이터 경합을 컴파일 단계에서 걸러 냅니다. 다른 언어라면 부하가 걸릴 때만 재현되던 종류의 버그가 빌드 실패로 앞당겨집니다.
성능과 안정성을 함께 챙긴다
GC가 없어 정지 시간이 끼어들지 않고, 메모리 사용이 코드 구조에서 바로 읽힙니다. 오래 떠 있는 데몬과 상한이 빡빡한 환경에서 특히 유리합니다.
코어 로직을 오래 믿고 간다
한 번 잘 짠 코드가 팀의 신뢰 자산으로 쌓이기 쉬워, 장애 비용이 큰 도메인에서 매력이 크다.
Ownership
소유권과 빌림을 몸으로 익히는 순서
첫 단계는 이동이 기본이라는 사실을 받아들이는 일입니다. Vec이나 String 같은 값을 다른 변수에 대입하거나 함수에 넘기면 복사되지 않고 소유권이 넘어갑니다. 넘긴 뒤 원래 이름을 다시 쓰면 컴파일 오류입니다. 이 규칙 하나가 “누가 이 메모리를 해제하는가”라는 질문을 프로그램 전체에서 없앱니다. 소유자는 항상 한 명이고, 그 이름이 범위를 벗어날 때 정리됩니다.
두 번째는 빌림 규칙입니다. 읽기 참조는 여러 개를 동시에 가질 수 있고, 쓰기 참조는 한 번에 하나만 존재할 수 있으며, 둘은 겹칠 수 없습니다. 순회 중에 원소를 추가하는 코드가 막히는 이유가 여기 있습니다. 다른 언어에서는 잘 돌다가 재할당이 일어나는 순간 깨지던 패턴입니다. 수명은 이 규칙의 시간 축입니다. 참조가 가리키는 값보다 참조가 더 오래 살면 안 된다는 조건을 컴파일러가 확인합니다.
세 번째는 동시성입니다. 값을 스레드 사이로 옮겨도 되는지, 참조를 공유해도 되는지를 타입이 표시합니다. 조건을 만족하지 않는 타입을 스레드에 넘기면 빌드가 실패하므로, 데이터 경합이 런타임 버그가 아니라 컴파일 오류가 됩니다. 여기까지 오면 Arc와 뮤텍스를 언제 써야 하는지가 자연스럽게 정리됩니다.
네 번째는 예외 처리에 해당하는 구조입니다. 트리나 그래프처럼 소유자가 하나로 떨어지지 않는 모양은 규칙과 정면으로 부딪힙니다. 이때 Rc나 Arc로 참조 수를 세고, RefCell로 검사를 실행 시점으로 미룹니다. 편법이 아니라 준비된 탈출구지만, 실행 중에 규칙을 어기면 패닉이 나므로 남용하면 컴파일러가 주는 이점이 줄어듭니다. 처음에는 데이터 구조를 소유권에 맞게 다시 그리는 쪽을 먼저 시도하는 편이 좋습니다.
동작을 공유하는 방식은 상속이 아니라 트레이트입니다. 타입에 트레이트를 나중에 구현해 붙일 수 있지만, 트레이트와 타입 중 최소 하나는 내 크레이트의 것이어야 한다는 제한이 있습니다. 남의 타입에 남의 트레이트를 구현할 수 없다는 뜻이고, 그럴 때는 얇은 래퍼 타입을 하나 만들어 감쌉니다. 이 규칙 덕분에 서로 다른 라이브러리가 같은 조합에 서로 다른 구현을 붙이는 충돌이 생기지 않습니다.
처음 3주 정도는 borrow checker와 싸우는 시간이라고 생각하고 일정을 잡으십시오. 이 구간에서 가장 빨리 빠져나오는 방법은 참조를 억지로 통과시키려 애쓰는 대신, 막힐 때마다 값을 복제해서라도 일단 돌아가게 만든 뒤 나중에 참조로 좁히는 것입니다. 성능 최적화는 그다음 문제입니다. 컴파일 오류 메시지도 끝까지 읽을 값어치가 있습니다. 대개 어떤 줄에서 빌림이 시작되고 어디서 끝나야 하는지까지 알려 줍니다.
No Null
null이 없다는 설계
Rust에는 null이 없습니다. 값이 없을 수 있으면 Option<T>로 감싸고, 실패할 수 있으면 Result<T, E>로 감쌉니다. 둘 다 그냥 열거형이라 특별한 문법이 아니고, 안의 값을 쓰려면 반드시 없는 경우를 처리해야 합니다. 다른 언어에서 가장 흔한 런타임 오류 하나가 타입 수준에서 사라지는 셈입니다.
대신 코드가 장황해질 수 있는데, 그래서 물음표 연산자가 있습니다. 함수 안에서 Result를 반환하는 호출 뒤에 물음표를 붙이면, 성공이면 값을 꺼내고 실패면 그 자리에서 에러를 반환합니다. 예외처럼 짧게 쓰면서도 어떤 호출이 실패할 수 있는지는 코드에 그대로 남습니다.
실무에서 중요한 부분은 에러 타입 설계입니다. 도메인 실패는 열거형으로 종류를 나눠 호출한 쪽이 분기할 수 있게 하고, 상위 계층에서는 문맥을 덧붙여 감쌉니다. 상태 자체도 문자열 대신 열거형으로 잠그면, 새로운 상태를 추가하는 순간 처리하지 않은 분기가 컴파일 오류로 드러납니다. 에러 타입을 계층별로 어떻게 나눌지는 Rust 에러 처리 가이드에서 예제로 볼 수 있습니다.
없을 수 있음과 실패할 수 있음
enum ChargeError {
UnknownTenant,
LimitExceeded { limit: u32 },
Gateway(String),
}
fn limit_of(cfg: &Config, tenant: &str) -> Option<u32> {
cfg.limits.get(tenant).copied()
}
fn charge(cfg: &Config, tenant: &str, amount: u32)
-> Result<Receipt, ChargeError>
{
let limit = limit_of(cfg, tenant)
.ok_or(ChargeError::UnknownTenant)?;
if amount > limit {
return Err(ChargeError::LimitExceeded { limit });
}
let auth = gateway::authorize(tenant, amount)
.map_err(ChargeError::Gateway)?;
Ok(Receipt::new(auth))
}
없을 수 있는 값과 실패할 수 있는 호출이 타입에 드러나 있고, 물음표가 실패 경로를 짧게 정리합니다.
Business Scenes
사업 현장에서 자주 보이는 장면
결제/정산 코어
트랜잭션 상태, 재시도 규칙, 동시 처리 안전성이 중요한 서비스에 잘 맞는다.
플랫폼 CLI/에이전트
배포 도구, 로그 수집기, 내부 에이전트처럼 가볍고 빠른 실행이 필요한 유틸리티에 강하다.
고성능 API
메모리 효율과 예측 가능한 지연이 중요한 백엔드에서 점차 채택 사례가 늘고 있다.
Workflow
팀이 움직이는 방식
- 도메인 상태를 enum으로 먼저 잠근다.
- 동시성 구조는 채널과 소유권 경계부터 문서화한다.
- 핵심 경로는 편의보다 실패 불가능성에 더 무게를 둔다.
- 러닝 커브를 낮추려면 코드 리뷰에서 설계 의도를 충분히 설명해야 한다.
상태를 강하게 고정하는 예시
enum PaymentState {
Authorized(String),
Captured(String),
Failed(String),
}
fn label(state: &PaymentState) -> &'static str {
match state {
PaymentState::Authorized(_) => "승인 완료",
PaymentState::Captured(_) => "정산 반영",
PaymentState::Failed(_) => "실패",
}
}
불안한 문자열 상태 대신 enum으로 잠그면 도메인 규칙을 컴파일러와 함께 지킬 수 있다.
Reading The Code
결제 상태 예제가 저렇게 생긴 이유
위 예제에서 상태를 문자열이 아니라 열거형으로 잡은 이유는 분기 검사 때문입니다. match는 모든 갈래를 다뤘는지 컴파일러가 확인합니다. 나중에 부분 취소 같은 상태가 하나 추가되면, 그 상태를 처리하지 않은 모든 match가 빌드 실패로 드러납니다. 문자열 상태였다면 어떤 분기는 조용히 “그 외”로 흘러갔을 자리입니다. 결제·정산처럼 상태를 빠뜨리는 것이 곧 사고인 도메인에서 이 검사는 무료 리뷰어에 가깝습니다.
각 갈래가 값을 함께 들고 있다는 점도 의도된 설계입니다. 승인 상태는 승인 번호를, 실패 상태는 실패 사유를 자기 안에 담습니다. 그래서 “승인인데 승인 번호가 없는” 조합 자체를 만들 수 없습니다. 구조체에 상태 필드와 선택적 필드를 늘어놓는 방식과 비교하면 차이가 분명합니다. 그쪽은 불가능한 조합을 코드로 표현할 수 있고, 그 조합이 언젠가 실제 데이터로 나타납니다.
반환 타입이 정적 문자열 참조인 것도 이유가 있습니다. 라벨은 프로그램 전체에 고정된 문자열이므로 새로 할당할 필요가 없고, 함수는 소유권을 넘기지 않고 참조만 돌려줍니다. 인자도 참조로 받기 때문에 호출한 쪽이 상태 값을 계속 갖고 있습니다. 값을 넘길지 빌려 줄지 매번 고르는 이 습관이 Rust 코드의 성능 특성을 만듭니다. 컬렉션에서 같은 판단을 어떻게 하는지는 컬렉션 가이드에 정리돼 있습니다.
Toolchain
cargo 하나로 도는 작업 흐름
Rust에서 도구를 고르느라 쓰는 시간은 거의 없습니다. cargo가 빌드, 의존성 관리, 테스트, 문서 생성, 배포까지 한 번에 담당합니다. 의존성은 매니페스트에 한 줄 적으면 crates.io에서 받아 오고, 잠금 파일이 팀 전체의 버전을 고정합니다. 여기에 서식은 cargo fmt, 린트는 cargo clippy가 표준으로 붙습니다. clippy는 단순한 스타일 검사기가 아니라 관용적이지 않은 패턴을 짚어 주기 때문에, 입문 단계에서는 사실상 코드 리뷰어 역할을 합니다.
테스트도 언어 안에 들어 있습니다. 같은 파일 안에 테스트 모듈을 두는 방식이 관례이고, 문서 주석에 적은 예제 코드까지 테스트로 실행됩니다. 문서와 코드가 어긋나면 빌드가 알려 준다는 뜻입니다. 빌드 모드는 두 가지를 구분해서 써야 합니다. 개발용 빌드는 검사가 많고 최적화가 없어 느리므로, 성능을 측정할 때는 반드시 릴리스 빌드로 재야 합니다. 개발 빌드 속도를 성능 결론으로 삼는 실수가 흔합니다.
배포 형태는 Go와 비슷하게 단순합니다. 대상 플랫폼을 지정해 크로스 컴파일할 수 있고, 산출물은 실행 파일 하나입니다. 여기에 Rust만의 선택지가 더 있습니다. C 호환 인터페이스로 라이브러리를 내보내면 Python이나 다른 언어에서 그대로 불러 쓸 수 있고, WebAssembly로 빌드하면 브라우저와 엣지 런타임에서 돌릴 수 있습니다. 전체를 Rust로 바꾸는 대신 무거운 코어만 Rust로 옮기고 나머지는 기존 언어로 두는 도입 방식이 현실적인 이유가 이것입니다. 측정과 튜닝 절차는 성능 가이드, 테스트 습관은 테스트 가이드에 이어집니다.
남는 비용은 컴파일 시간입니다. 의존성이 늘수록 초기 빌드가 길어지고, 큰 프로젝트에서는 이것이 개발 리듬에 영향을 줍니다. 검사만 하는 명령으로 빠르게 확인하고 전체 빌드는 덜 자주 돌리는 식의 요령이 필요합니다. 이 부담을 감수할 값어치가 있는지가 결국 도입 판단의 핵심입니다.
Decide
지금은 미루는 편이 나은 경우
- 장애 한 번의 비용이 큰 코어 로직이 있고 그 코드가 몇 년 살아야 한다면, 초기 속도를 내주고 얻는 것이 큽니다.
- GC 정지나 메모리 사용량이 운영 지표에 직접 잡히는 시스템이라면 대안이 많지 않습니다.
- 기존 서비스의 무거운 구간만 떼어 라이브러리로 붙일 계획이라면, 전면 재작성 없이 도입할 수 있습니다.
- 지금은 아닙니다 — 요구사항이 매주 바뀌는 초기 제품이라면 설계를 타입으로 잠그는 비용이 계속 청구됩니다.
- 지금은 아닙니다 — 마감이 몇 주 앞이고 팀에 Rust 경험자가 없다면, 3주쯤은 borrow checker에 쓰게 됩니다.
- 지금은 아닙니다 — 만들 것이 화면 위주의 CRUD이거나 한 번 쓰고 버릴 스크립트라면 얻는 것이 없습니다.
- 지금은 아닙니다 — 코드 리뷰에서 소유권 설계를 설명해 줄 사람이 한 명도 없다면, 각자 다른 방식으로 우회하다 코드가 갈라집니다.
먼저 감을 잡고 싶다면 Rust 학습 페이지의 예제를 훑고, 메모리 모델 자체가 낯설다면 메모리 기초 정리를 먼저 읽는 편이 순서가 맞습니다.
마이그레이션 저널
결제 코어 일부를 Rust로 옮긴 팀의 마이그레이션 저널
한 번의 대형 재작성은 없었다. 대신 장애 비용이 큰 경로부터 천천히 Rust로 옮기며 신뢰를 쌓았다.