
왜 Rust의 함수와 제어 흐름은 우리를 괴롭힐까요?
코드를 작성하다 보면 분명 논리적으로는 완벽해 보이는 함수를 만들었는데, 컴파일러가 빨간 줄을 그으며 거세게 항의하는 경험을 자주 하게 돼요. Rust 함수와 제어 흐름 실수는 대부분 프로그래밍 언어의 문법 자체를 몰라서가 아니라, Rust만의 독특한 소유권(Ownership) 규칙과 제어 흐름의 엄격함이 충돌하면서 발생해요.
파이썬이나 자바스크립트 같은 언어에서는 아무 문제 없이 넘어갔던 코드들이 Rust에서는 컴파일 에러를 뿜어내는 걸 보면 당혹스러울 수밖에 없어요. 함수에 데이터를 넘겨줄 때 이 데이터가 사라지는지, 아니면 빌려온 것인지 헷갈리기 시작하면 제어 흐름을 설계하는 것 자체가 큰 숙제가 되죠.
이런 문제는 단순히 에러를 고치는 것을 넘어, Rust가 메모리를 관리하는 근본적인 철학을 이해하지 못하면 계속해서 반복될 가능성이 높아요. 오늘 이 글을 통해 우리는 단순히 에러 메시지를 읽는 법을 넘어, Rust의 의도를 파악하고 효율적인 함수와 제어 구조를 설계하는 힘을 기르게 될 거예요.
이 글에서 함께 다룰 내용은 다음과 같아요.
- 함수의 반환 값과 식(Expression) 사이의 미묘한 차이
- 소유권 규칙을 준수하는 매개변수 설계 방법
- match 문과 if let을 활용한 안전한 제어 흐름 구축
- 실무에서 자주 마주치는 6가지 핵심 실수와 즉각적인 해결책
본격적인 코딩 전 점검할 Rust의 기본 개념
Rust 프로그래밍에서 함수와 제어 흐름을 제대로 다루려면, 가장 먼저 머릿속에 정리되어 있어야 할 몇 가지 핵심 개념이 있어요. 이 개념들이 흔들리면 함수 하나를 작성하더라도 소유권과 빌림(Borrowing)의 늪에 빠지기 쉽거든요.
데이터의 이동과 빌림의 차이
함수에 데이터를 전달할 때, 데이터가 함수 내부로 완전히 넘어가 버리는 Move 방식인지, 아니면 잠시 빌려주는 Borrow 방식인지를 명확히 구분해야 해요. Move가 발생하면 함수 호출 이후에 원래의 변수는 더 이상 사용할 수 없다는 점을 반드시 기억하세요. 반면 참조(&)를 사용한 빌림은 데이터를 안전하게 읽거나 수정할 수 있게 해주면서도 소유권은 유지해 줘요.
Rust의 모든 함수는 명시적으로 반환 타입을 지정하지 않으면 기본적으로 빈 튜플인
()를 반환해요. 이는 다른 언어의 void와 유사하지만, Rust에서는 식(Expression)을 통해 값을 반환하는 방식이 매우 중요하게 다뤄져요.제어 흐름의 종류와 선택 기준
Rust는 단순한 if-else 외에도 매우 강력한 match 문을 제공해요. 조건문을 선택할 때는 단순히 코드가 짧은 것을 고르는 게 아니라, 해당 조건이 모든 경우의 수를 안전하게 커버하고 있는지를 먼저 따져봐야 해요.
| 제어 방식 | 주요 용도 | 추천 상황 |
|---|---|---|
| if / else | 단순 논리 비교 | 조건이 2~3개 미만일 때 |
| match | 패턴 매칭 및 분기 | Enum 처리나 복잡한 분기 |
| if let | 특정 패턴 추출 | 하나의 케이스만 관심 있을 때 |
| loop / while | 반복 실행 | 무한 루프나 조건부 반복 |
이 표를 바탕으로 지금 내가 작성하려는 코드가 어떤 도구를 사용했을 때 가장 안전하고 읽기 쉬울지 미리 고민해 보는 과정이 필요해요. 특히 match 문을 사용할 때는 반드시 Exhaustiveness(완전성)를 체크해야 한다는 점을 잊지 마세요.
Rust 함수와 제어 흐름 마스터하기: 단계별 실행 가이드
이제 이론을 넘어 실제 코드를 어떻게 구성해야 하는지 구체적인 단계별 전략을 살펴볼게요. 이 과정은 단순히 문법을 배우는 게 아니라, Rust의 사고방식을 체득하는 과정이에요.
STEP 1. 함수의 반환 값과 식(Expression)의 차이 이해하기
Rust의 함수에서 가장 흔히 발생하는 실수 중 하나는 반환 값을 처리할 때 세미콜론(;)을 잘못 사용하는 거예요. Rust에서 함수의 마지막 줄에 세미콜론을 붙이면 그것은 문장(Statement)이 되어 값을 반환하지 않지만, 세미콜론을 생략하면 식(Expression)이 되어 그 값이 함수의 결과로 반환돼요.
예를 들어, 덧셈 결과를 반환하고 싶을 때 x + y;라고 쓰면 함수는 아무것도 반환하지 않는 () 타입을 반환하게 돼요. 반면 x + y라고 쓰면 계산된 결과값이 그대로 전달되죠. 이 차이는 아주 미세해 보이지만, 함수형 프로그래밍 스타일을 지향하는 Rust에서는 코드의 흐름을 결정짓는 매우 중요한 요소예요. 함수를 설계할 때 마지막 줄이 어떤 역할을 하는지 항상 의식적으로 확인하세요.
STEP 2. 소유권(Ownership)을 고려한 매개변수 설계
함수에 데이터를 넘겨줄 때는 이 데이터가 함수가 끝난 뒤에도 살아있어야 하는지를 먼저 결정해야 해요. 만약 함수 내부에서 데이터를 가공한 뒤에 원래 데이터도 계속 써야 한다면, 반드시 참조(&)를 사용하여 빌려오는 방식을 선택해야 해요.
만약 함수 매개변수에 String 타입을 그대로 적었다면, 그 함수를 호출하는 순간 소유권이 함수 안으로 넘어가 버려요. 호출한 쪽에서는 그 변수를 더 이상 사용할 수 없게 되어 컴파일 에러가 발생하죠. 이를 방지하기 위해 &str이나 &String처럼 참조자를 사용하는 습관을 들여야 해요. 데이터를 소유할 것인가, 아니면 잠시 빌려올 것인가를 결정하는 것이 함수 설계의 핵심이에요.
STEP 3. match 문을 활용한 강력하고 안전한 제어 흐름
Rust의 match 문은 단순한 분기문을 넘어선 강력한 도구예요. 특히 Enum(열거형)과 함께 사용할 때 그 진가가 드러나죠. match를 사용하면 개발자가 모든 가능한 경우의 수를 다루었는지 컴파일러가 검사해 주기 때문에, 런타임에 예상치 못한 오류로 프로그램이 죽는 일을 막을 수 있어요.
예를 들어, 네트워크 상태를 나타내는 Enum이 있다면 성공, 실패, 대기 상태를 모두 match로 처리해야 해요. 만약 개발자가 ‘대기’ 상태를 빼먹었다면 Rust 컴파일러는 즉시 에러를 띄우며 알려줄 거예요. 이런 엄격함 덕분에 우리는 훨씬 더 견고한 소프트웨어를 만들 수 있어요. 복잡한 조건문이 필요할수록 if-else를 길게 늘어뜨리기보다 match로 패턴을 구조화하는 것을 추천해요.
STEP 4. if let과 while let으로 코드 간결하게 만들기
모든 경우의 수를 다 다루어야 하는 match가 가끔은 너무 과하게 느껴질 때가 있어요. 오직 특정 패턴 하나에만 관심이 있고 나머지는 무시해도 될 때는 if let 문법이 아주 유용해요.
if let Some(value) = option_variable과 같은 방식은 match 문을 길게 쓰는 것보다 훨씬 가독성이 좋아요. 코드가 간결해지면 로직의 핵심이 더 잘 보이고, 유지보수도 쉬워지죠. 하지만 주의할 점은 if let을 남용하면 자칫 중요한 예외 상황을 놓칠 수도 있다는 점이에요. 패턴 매칭이 단순할 때만 if let을 쓰고, 구조가 복잡해지면 다시 match로 돌아오는 유연함이 필요해요.
STEP 5. 재귀 함수와 반복문의 효율적인 사용
제어 흐름의 마지막 단계는 루프를 제어하는 법이에요. Rust에는 loop, while, for라는 세 가지 주요 반복문이 있어요. 무조건적인 반복보다는 목적에 맞는 선택이 중요해요.
단순히 정해진 횟수만큼 반복한다면 for 문이 가장 안전하고 직관적이에요. 반면, 특정 조건이 만족될 때까지 계속 돌아야 한다면 while을 사용하죠. 만약 조건 없이 무한히 돌다가 내부에서 특정 시점에 탈출해야 한다면 loop 문을 쓰는 것이 가장 명확해요. 특히 loop 문은 break와 함께 값을 반환할 수 있는 독특한 기능을 가지고 있어, 상태 머신을 구현할 때 매우 강력한 도구가 돼요.
반복문 내부에서 변수의 소유권을 이동(Move)시킨 후, 다음 루프 회차에서 그 변수를 다시 사용하려고 하면 컴파일 에러가 발생해요. 반복문 안에서 데이터를 다룰 때는 항상 빌림(&)을 활용할지, 아니면 매번 새로운 데이터를 생성할지를 신중히 결정해야 합니다.
자주 하는 실수와 해결법
Rust를 배우는 과정에서 누구나 한 번쯤은 마주하게 되는 뼈아픈 실수들을 정리했어요. 이 패턴들을 미리 익혀두면 삽질하는 시간을 획기적으로 줄일 수 있어요.
- ❌ 함수 끝에 세미콜론을 붙여 값을 반환하지 못함
왜 발생하는가: 값을 반환하려던 식(Expression)을 문장(Statement)으로 만들어버렸기 때문이에요.
✅ 해결법: 마지막 줄의 세미콜론을 제거하여 식이 반환되도록 만드세요. - ❌ 함수 인자로 소유권을 넘겨버려 호출 후 변수 사용 불가
왜 발생하는가: 매개변수에 참조(&)가 아닌 값을 직접 전달하여 소유권이 이동했기 때문이에요.
✅ 해결법: 함수 정의를&T형태로 변경하여 데이터를 빌려오도록 수정하세요. - ❌ 함수 내부에서 생성한 지역 변수의 참조를 반환하려 함
왜 발생하는가: 함수가 종료되면 지역 변수는 메모리에서 사라지는데, 그 주소를 가리키는 참조를 넘기려 했기 때문이에요.
✅ 해결법: 값을 직접 반환(Move)하거나, 데이터의 생명주기(Lifetime)를 명시해야 해요. - ❌ match 문에서 모든 케이스를 다루지 않음
왜 발생하는가: Rust는 모든 가능성이 열려 있는 코드를 허용하지 않기 때문이에요.
✅ 해결법:_(와일드카드) 패턴을 사용하거나 모든 열거형 항목을 명시하세요. - ❌ 반복문 안에서 소유권을 가진 변수를 재사용하려 함
왜 발생하는가: 첫 번째 루프에서 변수가 함수나 다른 곳으로 이동해 버렸기 때문이에요.
✅ 해결법: 반복문 안에서는 소유권 이동 대신 참조(&)를 사용하세요. - ❌ if 문과 match 문의 적절한 사용 구분 실패
왜 발생하는가: 복잡한 패턴을 단순한 비교문으로 해결하려다 코드 가독성이 떨어지기 때문이에요.
✅ 해결법: 패턴 매칭이 필요하면 무조건match나if let을 사용하세요.
자주 묻는 질문
Q. 함수에서 return 키워드를 꼭 써야 하나요?
아니요, Rust에서는 함수의 마지막 줄에 세미콜론 없는 식을 쓰는 것만으로도 반환이 가능해요. 하지만 중간에 함수를 일찍 종료해야 할 때는 return 키워드를 명시적으로 사용해야 해요.
Q. &mut 참조와 & 참조의 차이가 정확히 무엇인가요?
&T는 데이터를 읽기만 할 수 있는 불변 참조이고, &mut T는 데이터를 수정할 수 있는 가변 참조예요. 단, Rust에서는 한 번에 하나의 가변 참조만 존재할 수 있다는 엄격한 규칙이 있어요.
Q. match 문에서 ‘_’ 패턴은 왜 쓰는 건가요?
_는 ‘나머지 모든 경우’를 뜻하는 와일드카드예요. 모든 케이스를 일일이 적기 번거롭거나, 특정 조건 외의 나머지는 모두 동일하게 처리하고 싶을 때 아주 유용하게 쓰여요.
Q. 왜 제 함수는 아무것도 반환하지 않는다고 나오나요?
함수의 마지막 줄에 세미콜론(;)을 붙였는지 확인해 보세요. 세미콜론을 붙이면 그 줄은 값을 반환하는 식이 아니라 실행 명령인 문장이 되어버려요.
Q. if let이 match보다 무조건 좋은 건가요?
그렇지 않아요. if let은 하나의 패턴만 처리할 때 코드를 줄여주지만, 여러 조건을 꼼꼼하게 따져야 하는 상황에서는 오히려 로직을 숨겨버려 위험할 수 있어요.
실전 적용을 위한 최종 점검 리스트
오늘 배운 내용을 바탕으로 코드를 작성할 때, 아래 리스트를 하나씩 체크해 보세요. 이 체크리스트만 지켜도 Rust 함수와 제어 흐름 실수의 90% 이상은 예방할 수 있어요.
- 함수 마지막 줄에 세미콜론을 붙여 반환 값을 날리고 있지는 않은가?
- 매개변수를 넘길 때 소유권을 넘길 것인가, 빌려줄 것인가 결정했는가?
- match 문을 사용할 때 모든 가능한 상황을 안전하게 커버했는가?
- 단순한 패턴 추출에는 if let을 사용하여 가독성을 높였는가?
- 반복문 내에서 데이터의 소유권이 의도치 않게 이동하지 않는가?
- 변수의 생명주기를 고려하여 안전한 참조를 사용하고 있는가?
이제 여러분은 단순한 문법 암기를 넘어, Rust 컴파일러와 대화하는 법을 배우기 시작했어요. 처음에는 컴파일 에러 메시지가 적처럼 느껴지겠지만, 사실 그것은 여러분의 코드를 더 안전하게 만들어주려는 가장 친절한 가이드예요.
오늘 바로 실행해 보세요!
- 오늘 할 일: 기존에 작성했던 Rust 함수 중 세미콜론 실수가 있었던 부분을 찾아 수정하기
- 이번 주 할 일: Enum을 정의하고 이를 match 문으로 완벽하게 처리하는 연습하기
- 실행 직전 할 일: 소유권 개념을 다시 한번 복습하고, 참조(&)를 활용한 함수 설계 연습하기
같은 실수를 반복하지 않도록 Rust 함수와 제어 흐름의 함정을 미리 익혀 두셨으니, 이제 훨씬 더 단단한 코드를 작성하실 수 있을 거예요. 계속해서 도전하세요!
함께 읽으면 좋은 글: Rust 함수와 제어 흐름 관련 다른 글과 입문 Rust 학습 가이드