
Rust 프로그래밍의 벽, 함수와 제어 흐름을 넘어서는 방법
코드를 열심히 작성했는데 갑자기 컴파일러가 빨간 줄을 띄우며 멈춰 세운 적이 있나요? 특히 소유권(Ownership) 문제로 인해 함수에 인자를 넘기거나, 조건문 안에서 변수를 사용하려고 할 때 발생하는 에러는 입문자를 가장 당혹스럽게 만들어요. 분명 논리적으로는 문제가 없어 보이는데, Rust 컴파일러는 왜 자꾸 허락해주지 않는지 답답할 때가 많을 거예요.
이런 문제는 단순히 문법을 몰라서라기보다, Rust가 함수와 제어 흐름을 다루는 독특한 철학을 아직 체득하지 못했기 때문에 발생해요. Rust는 다른 언어와 달리 함수 호출 시 데이터의 생명주기를 엄격하게 관리하고, 제어 흐름의 모든 경로가 명확하게 정의되기를 원해요. 이 규칙을 무시하고 코드를 짜면 결국 실행 단계에서 예상치 못한 패닉(Panic)을 마주하게 돼요.
이제 막 Rust의 세계에 발을 들인 개발자라면, 단순히 ‘돌아가는 코드’를 만드는 것을 넘어 ‘안전하고 효율적인 코드’를 설계하는 법을 배워야 해요. 오늘 소개할 Rust 함수와 제어 흐름 체크리스트는 여러분이 겪는 시행착오를 획기적으로 줄여줄 거예요. 이 가이드를 따라가다 보면 컴파일러와 싸우는 대신, 컴파일러를 든든한 조력자로 활용하는 법을 알게 될 거예요.
Rust의 함수와 제어 흐름은 단순히 명령을 나열하는 것이 아니라, 데이터의 흐름과 생명주기를 정의하는 과정이에요. 이 연결 고리를 이해하는 것이 핵심이에요.
이 글을 통해 여러분은 다음 내용을 완벽하게 익힐 수 있어요.
- 함수 설계 시 소유권과 참조를 결정하는 기준
- 다양한 제어 흐름(if, match, loop)을 상황에 맞게 선택하는 법
- 에러 처리를 통한 견고한 제어 흐름 설계
- 실무에 바로 적용 가능한 단계별 점검 항목
본격적인 구현 전, 반드시 갖춰야 할 기본 지식
함수를 만들고 조건문을 쓰기 전에, 우리가 어떤 도구를 가지고 있는지 명확히 알아야 해요. Rust는 표현식(Expression)과 문장(Statement)을 엄격하게 구분해요. 이 차이를 모르면 함수 마지막에 값을 반환하려고 할 때 왜 에러가 나는지 이해하기 어려워요. 문장은 값을 반환하지 않지만, 표현식은 값을 만들어내죠. Rust의 많은 제어 흐름이 표현식 기반이라는 점을 꼭 기억하세요.
또한, 함수를 설계할 때는 데이터가 ‘이동(Move)’할 것인지, 아니면 ‘빌려올(Borrow)’ 것인지를 먼저 결정해야 해요. 이를 결정하지 않고 코드를 작성하면, 함수 호출 한 번에 변수가 사라져 버리는 마법(?)을 경험하게 돼요. 따라서 구현에 들어가기 전, 자신의 데이터가 함수 내부에서 어떤 생명주기를 가질지 미리 머릿속으로 그려보는 연습이 필요해요.
상황별 제어 흐름 선택 기준
어떤 조건문이나 반복문을 쓸지 고민된다면 아래 표를 참고해서 결정해 보세요. 상황에 맞는 최적의 도구를 선택하는 것만으로도 코드의 가독성이 크게 올라가요.
| 구조 | 주요 용도 | 선택 기준 |
|---|---|---|
| if / else | 단순 조건 분기 | 조건이 한두 개로 단순할 때 |
| match | 복잡한 패턴 매칭 | 다양한 상태나 Enum을 다룰 때 |
| if let | 특정 패턴만 추출 | 특정 조건 하나만 관심 있을 때 |
| loop | 무한 반복 또는 값 반환 | 종료 조건이 불명확하거나 값을 반환할 때 |
| for | 이터레이터 순회 | 컬렉션의 요소를 하나씩 꺼낼 때 |
이 기준들은 단순히 문법을 선택하는 문제가 아니에요. 코드가 얼마나 예측 가능하고, 읽기 쉬우며, 안전한지를 결정하는 설계의 핵심이에요. 예를 들어, 단순히 `Option` 타입에서 값을 꺼내고 싶을 때 무분별하게 `match`를 쓰는 것보다 `if let`을 사용하는 것이 훨씬 간결하고 의도가 명확해져요.
모든 제어 흐름은 ‘완결성’을 가져야 해요. 특히 `match`를 사용할 때는 모든 가능한 경우의 수를 다루지 않으면 컴파일 에러가 발생한다는 점을 잊지 마세요.
실무에 바로 쓰는 단계별 구현 가이드
이제 구체적으로 어떻게 코드를 설계하고 작성해야 하는지 살펴볼게요. 단순히 문법을 나열하는 것이 아니라, 실무에서 마주치는 시나리오를 바탕으로 단계별로 접근해 봐요.
STEP 1. 함수 설계: 데이터의 소유권을 결정하세요
함수를 만들기 전 가장 먼저 해야 할 질문은 “이 함수가 데이터의 주인인가, 아니면 잠시 빌려 쓰는 것인가?”예요. 이 질문에 대한 답이 함수의 시그니처(Signature)를 결정해요.
- 소유권 이전(Move): 함수가 데이터를 완전히 가져와서 사용하거나 파괴해야 할 때 사용해요. 인자로 받은 데이터는 함수 호출 후 원래 위치에서 사용할 수 없게 돼요.
- 불변 참조(&T): 데이터를 읽기만 해야 할 때 사용해요. 가장 안전하고 흔한 방식이에요.
- 가변 참조(&mut T): 함수 내부에서 원본 데이터를 수정해야 할 때 사용해요. 한 번에 하나의 가변 참조만 존재할 수 있다는 규칙을 지켜야 해요.
설계 단계에서 이 부분을 명확히 하지 않으면, 함수를 호출할 때마다 소유권 에러를 해결하느라 시간을 다 허비하게 될 거예요.
STEP 2. 제어 흐름: 표현식 기반으로 사고하세요
Rust의 진정한 강력함은 제어 흐름이 표현식이라는 점에 있어요. 이는 변수에 값을 할당할 때 조건문을 직접 사용할 수 있다는 뜻이에요.
예를 들어, 값을 결정하는 로직을 별도의 함수로 빼지 않고도 이렇게 작성할 수 있어요.
let result = if score >= 90 { "A" } else { "B" };
이런 방식은 불필요한 가변 변수(mut)를 줄여주고, 값이 결정되는 순간을 명확하게 보여줘요. 함수 내부의 로직을 짤 때도 ‘어떤 조건에서 어떤 값을 반환할 것인가’를 하나의 흐름으로 연결하는 연습을 해보세요. 코드가 훨씬 간결해지고 버그가 줄어들 거예요.
STEP 3. 패턴 매칭: 모든 가능성을 열어두세요
`match` 문은 Rust의 꽃이에요. 하지만 단순히 값을 비교하는 용도로만 쓰기엔 아까워요. 구조체(Struct)나 열거형(Enum)의 내부 데이터를 분해(Destructuring)하면서 동시에 조건을 검사할 수 있거든요. 실무에서는 주로 `Result`나 `Option` 타입을 다룰 때 강력한 힘을 발휘해요.
패턴 매칭을 할 때는 반드시 모든 경로를 커버해야 해요. 만약 특정 케이스를 놓치고 싶다면 `_` (wildcard) 패턴을 사용하면 되지만, 이는 신중해야 해요. 나중에 새로운 상태(Enum variant)가 추가되었을 때, `_` 패턴 때문에 에러를 발견하지 못하고 넘어갈 수 있기 때문이에요. 새로운 타입을 추가했다면, `_`를 지우고 모든 케이스를 명시적으로 작성하는 것이 가장 안전한 실무 방식이에요.
STEP 4. 에러 흐름: 예외가 아닌 결과로 다루세요
다른 언어처럼 `try-catch`를 사용하는 대신, Rust는 에러를 결과 값(Result)으로 반환해요. 이는 에러가 발생할 수 있는 상황을 함수 시그니처에 명시적으로 드러내어, 호출하는 사람이 반드시 에러 처리를 고민하게 만들어요.
함수 내부에서 에러가 발생했을 때, 이를 즉시 반환할지(`?` 연산자 사용) 아니면 로직을 통해 처리할지를 결정해야 해요. `?` 연산자는 코드를 매우 깔끔하게 만들어주지만, 에러가 어디서 발생하는지 흐름을 놓치기 쉽게 만들기도 해요. 따라서 복잡한 로직에서는 에러를 전파하기 전에 적절한 컨텍스트를 추가하는 습관을 들이는 것이 좋아요.
실무 시나리오: 주문 처리 시스템 설계
이해를 돕기 위해 간단한 주문 처리 로직을 설계해 볼게요. 주문 상태에 따라 함수가 어떻게 움직여야 하는지 시나리오를 따라가 보세요.
- 데이터 정의: 주문 상태를 `Pending`, `Paid`, `Cancelled`로 정의한 Enum을 만들어요.
- 함수 설계: `process_order` 함수는 주문 정보를 참조(&Order)로 받고, 처리 결과를 `Result
`로 반환하도록 설계해요. - 제어 흐름 적용: `match` 문을 사용해 `Pending` 상태일 때만 결제 로직을 수행하고, 이미 결제되었거나 취소된 상태라면 에러를 반환해요.
- 결과 반환: 모든 조건이 충족되면 성공 상태를 반환하고, 결제 실패 시에는 구체적인 에러 원인을 담아 반환해요.
이렇게 설계하면 어떤 주문이 들어와도 예상치 못한 상태로 넘어가는 일이 없고, 모든 실패 케이스가 코드에 명확히 드러나게 돼요.
함수가 너무 길어지고 복잡한 제어 흐름을 가지고 있다면, 이는 함수가 너무 많은 일을 하고 있다는 신호예요. 기능을 더 작은 단위의 함수로 쪼개고, 각 함수가 단 하나의 책임만 갖도록 만드세요.
자주 하는 실수와 해결법
Rust를 배우면서 누구나 한 번쯤은 겪게 되는 실수들이 있어요. 에러 메시지가 길고 복잡해서 당황스럽겠지만, 패턴을 알면 금방 해결할 수 있어요.
- ❌ 실수: 함수에 인자를 넘겼는데 함수 호출 후 변수를 못 써요.
왜 발생하나요? 데이터의 소유권이 함수 내부로 이동(Move)되었기 때문이에요.
✅ 해결법: 데이터를 함수에 빌려주는 방식인 참조(&&T)를 사용하세요. - ❌ 실수: `match` 문에서 모든 경우의 수를 다 적지 않아 에러가 나요.
왜 발생하나요? Rust는 모든 가능성이 보장되지 않으면 안전하지 않다고 판단해요.
✅ 해결법: 누락된 패턴을 추가하거나, 명확한 의도가 있다면 `_` 패턴을 사용하세요. - ❌ 실수: `if` 문 안에서 정의한 변수를 밖에서 쓰려고 해요.
왜 발생하나요? 변수의 생명주기가 `if` 블록이 끝남과 동시에 종료되었기 때문이에요.
✅ 해결법: 변수를 `if` 블록 밖에서 먼저 선언하고, 블록 내부에서 값을 할당하세요. - ❌ 실수: `unwrap()`을 너무 자주 써서 프로그램이 갑자기 꺼져요.
왜 발생하나요? 값이 `None`이나 `Err`일 때 `unwrap()`은 즉시 패닉을 일으키기 때문이에요.
✅ 해결법: `match`나 `if let`, 또는 `?` 연산자를 사용해 에러를 안전하게 처리하세요. - ❌ 실수: 반복문 안에서 가변 참조를 여러 개 만들려고 해요.
왜 발생하나요? Rust의 대여 규칙(Borrowing rules)에 따라 가변 참조는 동시에 하나만 존재할 수 있어요.
✅ 해결법: 반복 범위(index)를 사용하거나, 데이터를 복사하여 사용하세요.
자주 묻는 질문
Q. 함수에서 값을 반환할 때 마지막 줄에 세미콜론(;)을 붙여야 하나요?
아니요, Rust에서 값을 반환하는 표현식으로 사용하려면 세미콜론을 빼야 해요. 세미콜론을 붙이면 그 문장은 ‘문장(Statement)’이 되어 값을 반환하지 않게 되고, 결과적으로 컴환 에러가 발생해요.
Q. `loop`와 `while` 중 무엇을 쓰는 게 더 좋을까요?
단순히 특정 조건이 충족될 때까지 반복한다면 `while`이 직관적이에요. 하지만 반복문 중간에 값을 계산해서 밖으로 전달해야 하거나, 무한 루프를 명시적으로 제어해야 한다면 `loop`를 사용하는 것이 훨씬 강력하고 표현력이 좋아요.
Q. 제어 흐름을 짤 때 성능 저하가 걱정돼요.
Rust의 `match`와 `if`는 컴파일 시점에 매우 최적화되어 있어요. 다른 언어의 동적 분기보다 빠를 때가 많으니 성능 걱정보다는 로직의 명확성에 더 집중하셔도 괜찮아요.
Q. `if let`은 언제 사용하는 게 가장 효율적인가요?
`match` 문에서 여러 패턴 중 딱 하나에만 관심이 있고, 나머지는 모두 무시해도 될 때 사용하세요. 코드가 훨씬 간결해지고 읽기 편해져요.
성공적인 Rust 코딩을 위한 마지막 점검
함수와 제어 흐름은 Rust 프로그래밍의 뼈대와 같아요. 이 뼈대가 튼튼해야 그 위에 쌓아 올리는 복잡한 로직들이 무너지지 않고 안전하게 동작할 수 있어요. 오늘 배운 체크리스트를 단순히 읽고 넘어가는 것이 아니라, 지금 작성 중인 코드에 직접 적용해 보는 것이 무엇보다 중요해요.
- 함수 설계 시 소유권(Move)과 참조(Borrow)를 먼저 결정하세요.
- 제어 흐름을 작성할 때는 표현식(Expression) 중심으로 코드를 짜세요.
- `match` 문은 모든 가능한 패턴을 포함하는지 확인하세요.
- `unwrap()` 대신 `?`나 `match`를 사용해 에러를 안전하게 다루세요.
- 불필요한 가변 변수(mut) 사용을 최소화하여 데이터 흐름을 단순화하세요.
이제 실천할 차례예요. 오늘 배운 내용을 바탕으로 다음 단계를 진행해 보세요.
- 오늘 할 일: 현재 작성 중인 함수 중 하나를 골라, 인자가 소유권을 가져가는지 참조를 가져가는지 검토해 보세요.
- 이번 주 할 일: `match`와 `if let`을 적절히 섞어 사용하여 조건문 로직을 리팩토링해 보세요.
- 실행 직전 할 일: 코드를 배포하기 전, 모든 `unwrap()`이 안전한지 혹은 에러 처리가 되어 있는지 최종 점검하세요!
배포 전 Rust 함수와 제어 흐름 체크리스트를 통해 여러분의 코드를 한 번 더 확인한다면, 훨씬 더 신뢰할 수 있는 프로그램을 만들 수 있을 거예요. Rust는 처음엔 까다롭지만, 그 규칙을 따르는 순간 가장 강력한 무기가 되어줄 거예요.
Rust 함수와 제어 흐름에 대해 더 깊이 알고 싶다면, 입문 Rust 학습 가이드나 Rust 소유권 마스터하기와 같은 관련 글들도 함께 읽어보시는 것을 추천드려요.