이미지 로딩 중...

Rust 입문 가이드 34 함수 정의와 매개변수 - 슬라이드 1/9
A

AI Generated

2025. 11. 13. · 2 Views

Rust 입문 가이드 34 함수 정의와 매개변수

Rust에서 함수를 정의하고 매개변수를 다루는 방법을 실무 중심으로 배웁니다. 기본 함수 정의부터 다양한 매개변수 패턴, 반환값 처리까지 초급 개발자가 바로 활용할 수 있는 내용을 다룹니다.


목차

  1. 기본 함수 정의 - fn 키워드로 함수 만들기
  2. 매개변수가 있는 함수 - 값을 받아서 처리하기
  3. 반환값이 있는 함수 - 결과를 돌려주기
  4. 표현식과 문장 - 세미콜론의 의미
  5. 다양한 반환 타입 - 숫자, 문자열, 튜플
  6. 함수 시그니처와 타입 추론 - 명시적 vs 암묵적
  7. 조기 반환 패턴 - return으로 빠져나가기
  8. 함수 오버로딩은 없다 - Rust의 선택

1. 기본 함수 정의 - fn 키워드로 함수 만들기

시작하며

여러분이 Rust로 첫 프로젝트를 시작할 때, main 함수 외에 다른 함수를 어떻게 만드는지 막막했던 적 있나요? "fn 키워드는 봤는데, 정확히 어떻게 사용해야 할지 모르겠어요"라는 고민을 많이 하실 겁니다.

함수는 코드를 재사용 가능한 단위로 나누는 가장 기본적인 방법입니다. Rust에서는 함수 정의가 매우 명확하고 엄격한 규칙을 따르기 때문에, 한 번 제대로 배워두면 실수를 크게 줄일 수 있습니다.

바로 이럴 때 필요한 것이 fn 키워드를 사용한 기본 함수 정의입니다. 간단한 규칙만 익히면 깔끔하고 읽기 좋은 코드를 작성할 수 있습니다.

개요

간단히 말해서, Rust의 함수는 fn 키워드로 시작하여 함수명, 매개변수 목록, 반환 타입, 그리고 함수 본문으로 구성됩니다. 함수명은 스네이크 케이스(snake_case)로 작성하는 것이 Rust의 관례입니다.

예를 들어, calculate_total이나 print_message 같은 형식입니다. 이는 코드의 일관성을 유지하고 다른 Rust 개발자들과 협업할 때 중요합니다.

기존 C나 Java에서는 함수명에 카멜케이스를 사용했다면, Rust에서는 스네이크 케이스를 사용하여 변수명과 함수명을 구분합니다. 이렇게 하면 코드를 읽을 때 어떤 것이 함수이고 어떤 것이 타입인지 한눈에 파악할 수 있습니다.

Rust 함수의 핵심 특징은 타입 안정성, 명확한 시그니처, 그리고 표현식 기반 반환입니다. 타입 안정성 덕분에 컴파일 타임에 많은 오류를 잡을 수 있고, 명확한 시그니처로 인해 함수의 계약(contract)이 분명해집니다.

코드 예제

// 매개변수도 반환값도 없는 가장 기본적인 함수
fn greet() {
    println!("안녕하세요, Rust 세계에 오신 것을 환영합니다!");
}

// main 함수에서 호출
fn main() {
    // 함수 호출은 함수명 뒤에 괄호를 붙입니다
    greet();
    greet(); // 여러 번 호출 가능
}

설명

이것이 하는 일: 위 코드는 greet이라는 이름의 함수를 정의하고, main 함수에서 이를 두 번 호출하여 환영 메시지를 출력합니다. 첫 번째로, fn greet() 부분은 함수 정의의 시작을 나타냅니다.

fn은 "function"의 약자로, Rust 컴파일러에게 "지금부터 함수를 정의하겠습니다"라고 알려주는 키워드입니다. 빈 괄호 ()는 이 함수가 매개변수를 받지 않는다는 의미이고, 반환 타입을 명시하지 않았으므로 이 함수는 값을 반환하지 않습니다(정확히는 unit 타입 ()을 반환).

그 다음으로, 중괄호 {} 안의 코드가 실행되면서 println! 매크로를 호출합니다.

이 매크로는 표준 출력으로 텍스트를 출력하는 역할을 하며, 느낌표(!)는 이것이 함수가 아닌 매크로임을 나타냅니다. Rust에서 매크로는 컴파일 타임에 코드를 생성하는 강력한 도구입니다.

마지막으로, main 함수에서 greet()를 두 번 호출하면 같은 메시지가 두 번 출력됩니다. 함수를 정의해두면 필요할 때마다 같은 코드를 반복해서 작성할 필요 없이 함수명만 호출하면 됩니다.

여러분이 이 코드를 사용하면 코드 중복을 제거하고, 유지보수가 쉬운 구조를 만들 수 있습니다. 예를 들어, 환영 메시지를 변경하고 싶다면 greet 함수 내부만 수정하면 모든 호출 지점에 반영됩니다.

실전 팁

💡 함수명은 항상 소문자 스네이크 케이스를 사용하세요. rustc 컴파일러는 이를 따르지 않으면 경고를 표시합니다.

💡 함수는 사용하기 전에 정의되어 있어야 하지만, Rust는 main 함수 이전이든 이후든 어디에 정의해도 괜찮습니다. 순서가 자유롭습니다.

💡 함수가 하는 일을 명확히 나타내는 동사 중심의 이름을 사용하세요. calculate, process, validate 같은 동사로 시작하면 좋습니다.

💡 너무 긴 함수는 여러 개의 작은 함수로 분리하세요. 일반적으로 한 함수는 한 가지 일만 해야 합니다(Single Responsibility Principle).


2. 매개변수가 있는 함수 - 값을 받아서 처리하기

시작하며

여러분이 사용자 이름을 받아서 개인화된 인사말을 출력하고 싶을 때, 같은 함수를 매번 다르게 작동시켜야 하는 상황을 겪어본 적 있나요? "함수는 만들었는데, 어떻게 외부에서 값을 전달하지?"라는 고민이 생깁니다.

이런 문제는 실제 개발 현장에서 매우 자주 발생합니다. 함수가 고정된 동작만 한다면, 코드의 재사용성이 크게 떨어지고 비슷한 함수를 여러 개 만들어야 하는 비효율이 생깁니다.

바로 이럴 때 필요한 것이 매개변수(parameters)입니다. 매개변수를 통해 함수에 값을 전달하면, 같은 로직으로 다양한 입력을 처리할 수 있습니다.

개요

간단히 말해서, 매개변수는 함수가 외부로부터 값을 받기 위한 통로입니다. Rust에서는 매개변수를 정의할 때 반드시 타입을 명시해야 합니다.

매개변수는 함수 시그니처의 일부로, 함수가 어떤 타입의 값을 몇 개 받는지를 명확히 정의합니다. 예를 들어, 사용자 이름(문자열)과 나이(정수)를 받아 프로필을 출력하는 함수를 만들 때 유용합니다.

Rust의 타입 시스템 덕분에 잘못된 타입의 값을 전달하면 컴파일 에러가 발생하여 런타임 오류를 사전에 방지할 수 있습니다. 기존에는 함수를 여러 개 만들어야 했다면, 이제는 매개변수를 사용하여 하나의 함수로 다양한 상황을 처리할 수 있습니다.

이는 Don't Repeat Yourself(DRY) 원칙을 실천하는 핵심 방법입니다. 매개변수의 핵심 특징은 타입 안정성, 명시적 선언, 그리고 불변성 기본 원칙입니다.

Rust는 기본적으로 매개변수를 불변(immutable)으로 취급하여 함수 내부에서 실수로 값을 변경하는 것을 방지합니다.

코드 예제

// 하나의 매개변수를 받는 함수
fn greet_user(name: &str) {
    // name은 문자열 슬라이스 타입
    println!("안녕하세요, {}님!", name);
}

// 여러 개의 매개변수를 받는 함수
fn print_user_info(name: &str, age: u32) {
    // 쉼표로 매개변수를 구분
    println!("이름: {}, 나이: {}세", name, age);
}

fn main() {
    greet_user("철수");
    print_user_info("영희", 25);
}

설명

이것이 하는 일: 위 코드는 문자열과 숫자를 매개변수로 받아 개인화된 메시지를 출력하는 두 개의 함수를 정의하고 사용합니다. 첫 번째로, greet_user 함수의 name: &str 부분은 "name이라는 이름의 매개변수를 선언하며, 그 타입은 문자열 슬라이스(&str)입니다"라는 의미입니다.

&str은 Rust에서 문자열을 빌려오는 가장 일반적인 방법으로, 소유권을 가져오지 않고 읽기만 합니다. 이렇게 하면 함수 호출 후에도 원본 문자열을 계속 사용할 수 있습니다.

그 다음으로, print_user_info 함수는 두 개의 매개변수를 받습니다. 매개변수가 여러 개일 때는 쉼표로 구분하며, 각각의 매개변수는 독립적인 타입을 가집니다.

age: u32는 부호 없는 32비트 정수를 의미하며, 나이처럼 음수가 될 수 없는 값에 적합합니다. Rust에서는 각 정수 타입의 범위를 명확히 하여 메모리를 효율적으로 사용합니다.

마지막으로, main 함수에서 실제 값(arguments)을 전달하여 함수를 호출합니다. "철수"와 "영희"는 문자열 리터럴이며, 25는 정수 리터럴입니다.

Rust 컴파일러는 전달된 값의 타입이 함수 시그니처와 일치하는지 확인하고, 일치하지 않으면 컴파일 에러를 발생시킵니다. 여러분이 이 코드를 사용하면 타입 안정성을 확보하고, 함수의 인터페이스를 명확히 할 수 있습니다.

또한 문서화 없이도 함수 시그니처만 보고 어떤 값을 전달해야 하는지 알 수 있어 협업이 수월해집니다.

실전 팁

💡 매개변수 타입은 생략할 수 없습니다. 타입 추론이 되는 변수와 달리, 함수 시그니처는 명시적이어야 합니다.

💡 문자열을 받을 때는 대부분 &str을 사용하세요. String을 받으면 소유권이 이동하여 호출 후 원본을 사용할 수 없게 됩니다.

💡 매개변수는 기본적으로 불변입니다. 함수 내부에서 변경하려면 mut 키워드를 추가해야 하지만, 일반적으로 권장되지 않습니다.

💡 매개변수가 3개 이상이면 구조체로 묶는 것을 고려하세요. 가독성이 좋아지고 관련 데이터를 그룹화할 수 있습니다.

💡 숫자 타입을 선택할 때는 값의 범위를 고려하세요. 나이는 u8(0-255)로도 충분하지만, u32를 사용하면 더 안전합니다.


3. 반환값이 있는 함수 - 결과를 돌려주기

시작하며

여러분이 두 숫자를 더한 결과를 다른 곳에서 사용하고 싶을 때, "함수에서 계산한 값을 어떻게 가져오지?"라는 고민을 해본 적 있나요? 단순히 출력만 하는 함수로는 계산 결과를 재사용할 수 없습니다.

이런 문제는 실제 프로그래밍에서 핵심적인 부분입니다. 함수가 계산한 값을 변수에 저장하거나 다른 함수에 전달하려면, 반드시 값을 반환할 수 있어야 합니다.

그렇지 않으면 같은 계산을 여러 번 반복해야 하는 비효율이 생깁니다. 바로 이럴 때 필요한 것이 반환값(return value)입니다.

Rust는 표현식 기반 언어로, 함수의 마지막 표현식이 자동으로 반환값이 되는 독특한 특징을 가지고 있습니다.

개요

간단히 말해서, 반환값은 함수가 계산한 결과를 호출자에게 돌려주는 방법입니다. Rust에서는 화살표 ->를 사용하여 반환 타입을 명시합니다.

반환값을 사용하면 함수를 순수 함수(pure function)처럼 만들 수 있습니다. 예를 들어, 가격 계산, 데이터 변환, 유효성 검사 같은 작업에서 결과를 반환하면 테스트하기 쉽고 재사용성이 높은 코드를 작성할 수 있습니다.

특히 함수형 프로그래밍 패턴을 적용할 때 반환값은 필수적입니다. 기존에는 전역 변수나 참조를 통해 값을 전달했다면, Rust에서는 명시적인 반환값으로 데이터 흐름을 명확하게 만듭니다.

이는 코드를 읽는 사람이 함수의 입력과 출력을 한눈에 파악할 수 있게 해줍니다. 반환값의 핵심 특징은 타입 명시, 표현식 기반 반환, 그리고 세미콜론의 의미입니다.

Rust에서는 세미콜론이 있으면 문장(statement)이 되어 값을 반환하지 않고, 세미콜론이 없으면 표현식(expression)이 되어 값을 반환합니다. 이는 Rust의 독특한 특징 중 하나입니다.

코드 예제

// 두 숫자를 더해서 결과를 반환
fn add(a: i32, b: i32) -> i32 {
    // 마지막 표현식이 자동으로 반환됨 (세미콜론 없음!)
    a + b
}

// return 키워드를 명시적으로 사용하는 방법
fn subtract(a: i32, b: i32) -> i32 {
    return a - b; // 세미콜론 있어도 됨
}

fn main() {
    let sum = add(10, 20); // 30
    let diff = subtract(50, 15); // 35
    println!("합: {}, 차: {}", sum, diff);
}

설명

이것이 하는 일: 위 코드는 두 가지 방법으로 값을 반환하는 함수를 보여줍니다. 하나는 Rust 스타일의 암묵적 반환, 다른 하나는 명시적 return 키워드 사용입니다.

첫 번째로, add 함수의 -> i32 부분은 "이 함수는 32비트 부호 있는 정수를 반환합니다"라는 의미입니다. 함수 본문의 마지막 줄인 a + b는 세미콜론이 없기 때문에 표현식으로 취급되어 그 값이 자동으로 반환됩니다.

이것이 Rust의 "표현식 기반" 특성이며, 함수형 프로그래밍 언어에서 많이 사용되는 패턴입니다. 그 다음으로, subtract 함수는 return 키워드를 명시적으로 사용합니다.

이 방식은 C, Java, Python 등 다른 언어와 유사하며, 특히 함수 중간에서 조기 반환(early return)이 필요할 때 유용합니다. return을 사용하면 세미콜론을 붙여도 되고 안 붙여도 되지만, 일관성을 위해 보통 붙입니다.

마지막으로, main 함수에서 반환값을 변수에 저장합니다. let sum = add(10, 20)은 add 함수를 호출하고 그 반환값(30)을 sum 변수에 바인딩합니다.

이렇게 하면 계산 결과를 다른 곳에서 재사용할 수 있으며, 함수 체이닝이나 복잡한 계산을 구성할 수 있습니다. 여러분이 이 코드를 사용하면 함수를 조합 가능한(composable) 단위로 만들 수 있습니다.

예를 들어, add(10, subtract(50, 15))처럼 함수 호출을 중첩하여 복잡한 계산을 표현할 수 있고, 테스트할 때는 반환값을 검증하기만 하면 되므로 매우 간단합니다.

실전 팁

💡 Rust 스타일은 마지막 표현식으로 반환하는 것입니다. 세미콜론을 빼먹지 않도록 주의하세요!

💡 함수 중간에서 조기 반환할 때만 return 키워드를 사용하세요. 일관성 있는 코드 스타일이 중요합니다.

💡 반환 타입을 생략하면 unit 타입 ()을 반환하는 것으로 간주됩니다. 명시적으로 -> ()를 쓸 필요는 없습니다.

💡 복잡한 로직에서는 중간 결과를 변수에 저장하고 마지막에 그 변수를 반환하세요. 가독성이 좋아집니다.

💡 Result나 Option 타입을 반환하면 에러 처리를 우아하게 할 수 있습니다. 실무에서 매우 자주 사용되는 패턴입니다.


4. 표현식과 문장 - 세미콜론의 의미

시작하며

여러분이 Rust 코드를 작성하다가 "세미콜론 하나 때문에 에러가 난다"는 경험을 해본 적 있나요? "type mismatch: expected i32, found ()"라는 컴파일 에러는 Rust 초보자가 가장 자주 겪는 당혹스러운 순간입니다.

이런 문제는 Rust의 표현식(expression) 기반 철학을 이해하지 못해서 발생합니다. 다른 언어에서는 세미콜론이 단순히 문장의 끝을 나타내지만, Rust에서는 세미콜론이 값의 반환 여부를 결정하는 중요한 역할을 합니다.

바로 이럴 때 필요한 것이 표현식과 문장(statement)의 차이를 이해하는 것입니다. 이 개념을 정확히 알면 Rust의 함수 반환 메커니즘을 완벽히 이해할 수 있습니다.

개요

간단히 말해서, 표현식은 값을 생성하는 코드이고, 문장은 동작을 수행하지만 값을 반환하지 않는 코드입니다. Rust에서는 거의 모든 것이 표현식입니다.

표현식과 문장의 구분은 Rust의 핵심 철학입니다. 예를 들어, if 블록, match 블록, 심지어 블록 자체도 표현식이 될 수 있습니다.

이는 함수형 프로그래밍의 영향을 받은 것으로, 코드를 더 간결하고 표현력 있게 만들어줍니다. 기존 C나 Java에서는 if가 제어 구조일 뿐이었다면, Rust에서는 if도 값을 반환하는 표현식이 될 수 있습니다.

이는 삼항 연산자 없이도 조건부 값 할당을 깔끔하게 할 수 있게 해줍니다. 표현식의 핵심 특징은 값 생성, 중첩 가능성, 그리고 세미콜론에 의한 문장 변환입니다.

세미콜론을 붙이면 표현식이 문장으로 변환되어 값을 반환하지 않게 됩니다. 이는 의도하지 않은 값 반환을 방지하는 명시적인 방법입니다.

코드 예제

fn expression_example() -> i32 {
    // 블록도 표현식! 마지막 줄이 반환됨
    let x = {
        let price = 100;
        let quantity = 5;
        price * quantity // 500 (세미콜론 없음)
    };

    // if도 표현식이므로 값을 반환할 수 있음
    let discount = if x > 400 { 50 } else { 20 };

    // 최종 결과 반환
    x - discount
}

fn main() {
    println!("결과: {}", expression_example()); // 450
}

설명

이것이 하는 일: 위 코드는 Rust에서 블록, if, 그리고 함수 반환이 모두 표현식으로 작동하는 것을 보여줍니다. 첫 번째로, let x = { ...

} 부분은 블록 표현식입니다. 중괄호로 감싸진 블록은 그 자체로 하나의 표현식이 될 수 있으며, 블록 내부의 마지막 표현식이 블록의 값이 됩니다.

이 경우 price * quantity가 세미콜론 없이 작성되었므로 500이라는 값이 x에 바인딩됩니다. 블록 표현식은 복잡한 계산을 논리적으로 그룹화하면서도 값을 생성할 수 있게 해줍니다.

그 다음으로, if 표현식은 조건에 따라 다른 값을 반환합니다. x > 400이 참이면 50을, 거짓이면 20을 반환하여 discount 변수에 저장합니다.

중요한 점은 if의 각 분기(branch)가 같은 타입의 값을 반환해야 한다는 것입니다. 한 쪽은 i32를 반환하고 다른 쪽은 String을 반환하면 컴파일 에러가 발생합니다.

이는 타입 안정성을 보장하는 Rust의 방식입니다. 마지막으로, 함수의 마지막 줄인 x - discount는 세미콜론 없이 작성되어 함수의 반환값이 됩니다.

만약 x - discount; 처럼 세미콜론을 붙이면 이것은 문장이 되어 값을 반환하지 않고, 함수는 unit 타입 ()을 반환하게 되어 컴파일 에러가 발생합니다. 여러분이 이 코드를 사용하면 변수 선언을 줄이고 코드를 더 간결하게 만들 수 있습니다.

특히 if 표현식은 삼항 연산자를 대체하며 더 읽기 쉬운 코드를 만들어주고, 블록 표현식은 복잡한 초기화 로직을 깔끔하게 처리할 수 있게 해줍니다.

실전 팁

💡 반환하고 싶은 값에는 절대 세미콜론을 붙이지 마세요. 이것이 Rust 초보자의 가장 흔한 실수입니다.

💡 if 표현식의 모든 분기는 같은 타입을 반환해야 합니다. 타입이 다르면 컴파일 에러가 발생합니다.

💡 블록 표현식을 사용하면 복잡한 초기화 로직을 변수 선언과 함께 작성할 수 있습니다. 가독성이 크게 향상됩니다.

💡 match도 강력한 표현식입니다. 패턴 매칭과 값 반환을 동시에 할 수 있어 매우 유용합니다.

💡 디버깅할 때 세미콜론을 일시적으로 추가했다 제거하는 것을 잊지 마세요. 반환 타입 에러의 원인이 됩니다.


5. 다양한 반환 타입 - 숫자, 문자열, 튜플

시작하며

여러분이 함수에서 여러 개의 값을 동시에 반환하고 싶을 때, "하나의 값만 반환할 수 있는데 어떻게 하지?"라는 고민을 해본 적 있나요? 또는 문자열을 반환할 때 String과 &str 중 무엇을 선택해야 할지 혼란스러웠던 경험이 있을 겁니다.

이런 문제는 실무에서 매우 자주 발생합니다. 함수가 계산한 결과와 함께 성공/실패 여부를 반환하거나, 좌표처럼 관련된 여러 값을 함께 반환해야 하는 경우가 많습니다.

단일 값만 반환할 수 있다면 코드가 복잡해지고 가독성이 떨어집니다. 바로 이럴 때 필요한 것이 다양한 반환 타입을 활용하는 것입니다.

Rust는 튜플, 구조체, 열거형 등 여러 값을 그룹화하여 반환할 수 있는 풍부한 타입 시스템을 제공합니다.

개요

간단히 말해서, Rust 함수는 기본 타입(i32, f64, bool 등)뿐만 아니라 문자열, 튜플, 구조체 등 다양한 타입을 반환할 수 있습니다. 반환 타입의 선택은 함수의 목적과 성능에 큰 영향을 미칩니다.

예를 들어, 문자열을 반환할 때 &str을 사용하면 메모리 할당 없이 빠르지만 생명주기 제약이 있고, String을 사용하면 소유권을 가지지만 힙 할당이 발생합니다. 실무에서는 이러한 트레이드오프를 이해하고 적절히 선택해야 합니다.

기존에는 구조체나 클래스를 만들어야 했던 상황에서, Rust에서는 가벼운 튜플로 빠르게 여러 값을 반환할 수 있습니다. 튜플은 이름 없는 필드들의 집합으로, 간단한 다중 반환에 매우 유용합니다.

반환 타입의 핵심 특징은 타입 안정성, 소유권 의미, 그리고 구조 분해입니다. 튜플을 반환하면 패턴 매칭을 통해 각 요소를 쉽게 추출할 수 있고, 이는 함수형 프로그래밍 스타일을 가능하게 합니다.

코드 예제

// 튜플로 여러 값 반환
fn calculate_stats(numbers: &[i32]) -> (i32, i32, f64) {
    let sum: i32 = numbers.iter().sum();
    let count = numbers.len() as i32;
    let average = sum as f64 / count as f64;

    // (최소값, 최대값, 평균) 튜플 반환
    (*numbers.iter().min().unwrap(),
     *numbers.iter().max().unwrap(),
     average)
}

// String 반환
fn format_greeting(name: &str) -> String {
    format!("안녕하세요, {}님!", name)
}

fn main() {
    let data = vec![10, 20, 30, 40, 50];
    let (min, max, avg) = calculate_stats(&data);
    println!("최소: {}, 최대: {}, 평균: {:.2}", min, max, avg);

    let greeting = format_greeting("Rust");
    println!("{}", greeting);
}

설명

이것이 하는 일: 위 코드는 통계 계산 결과를 튜플로 반환하고, 동적 문자열을 생성하여 반환하는 두 가지 패턴을 보여줍니다. 첫 번째로, calculate_stats 함수는 슬라이스(&[i32])를 받아 세 개의 값을 담은 튜플 (i32, i32, f64)를 반환합니다.

슬라이스를 매개변수로 받으면 배열이나 벡터 모두 전달할 수 있어 유연합니다. 함수 내부에서 iter()를 사용하여 이터레이터를 만들고, sum(), min(), max() 같은 메서드를 체이닝하여 값을 계산합니다.

unwrap()은 Option을 벗겨내는데, 실무에서는 에러 처리를 더 신중하게 해야 하지만 예제에서는 간단히 처리했습니다. 그 다음으로, 튜플 반환에서 괄호 안에 세 개의 값을 쉼표로 구분하여 작성합니다.

첫 번째와 두 번째 요소는 i32 타입이고, 세 번째는 f64 타입입니다. Rust는 이를 하나의 값으로 취급하여 반환합니다.

튜플의 장점은 관련된 값들을 빠르게 그룹화할 수 있다는 것이며, 단점은 필드 이름이 없어 인덱스로만 접근해야 한다는 것입니다. 그리고 format_greeting 함수는 String 타입을 반환합니다.

format! 매크로는 새로운 String을 힙에 할당하여 생성하므로, 이 함수는 소유권을 가진 문자열을 반환합니다.

호출자는 이 문자열을 자유롭게 사용하고 수정할 수 있습니다. &str을 반환하려면 함수 내부에 문자열 리터럴이 있어야 하는데, 동적으로 생성된 문자열은 String으로만 반환할 수 있습니다.

마지막으로, main 함수에서 let (min, max, avg) = ...처럼 구조 분해(destructuring)를 사용하여 튜플의 각 요소를 개별 변수에 바인딩합니다. 이는 매우 편리한 패턴으로, 튜플의 요소를 하나씩 추출하는 코드를 작성할 필요가 없습니다.

여러분이 이 코드를 사용하면 복잡한 구조체를 만들지 않고도 관련된 여러 값을 반환할 수 있습니다. 특히 간단한 계산 함수에서 튜플은 매우 유용하며, 구조 분해를 통해 코드 가독성도 좋아집니다.

또한 String 반환은 동적 문자열 생성이 필요할 때 표준적인 패턴입니다.

실전 팁

💡 튜플 요소가 3개를 넘으면 구조체를 사용하는 것이 좋습니다. 가독성과 유지보수성이 크게 향상됩니다.

💡 &str은 빌림, String은 소유권입니다. 함수 내부에서 문자열을 생성하면 String을 반환해야 합니다.

💡 구조 분해 시 일부 값만 필요하면 언더스코어(_)를 사용하세요. let (min, _, avg) = ...처럼 쓸 수 있습니다.

💡 Option<T>나 Result<T, E>를 반환하면 에러 처리를 우아하게 할 수 있습니다. unwrap() 대신 ?를 사용하세요.

💡 성능이 중요하면 참조를 반환하는 것을 고려하세요. 다만 생명주기 문제를 조심해야 합니다.


6. 함수 시그니처와 타입 추론 - 명시적 vs 암묵적

시작하며

여러분이 Rust를 배우면서 "왜 함수에서는 타입을 꼭 써야 하는데, 변수는 타입을 생략해도 되지?"라는 의문을 가져본 적 있나요? let x = 10;은 타입을 안 써도 되는데, 함수 매개변수에서는 fn add(a, b)가 안 되는 이유가 궁금하셨을 겁니다.

이런 차이는 Rust의 설계 철학과 관련이 있습니다. 변수는 함수 내부의 구현 세부사항이지만, 함수 시그니처는 외부와의 계약(contract)이기 때문에 명시적이어야 합니다.

타입이 불명확한 함수는 사용하기 어렵고 오류를 유발하기 쉽습니다. 바로 이럴 때 알아야 할 것이 함수 시그니처의 중요성입니다.

명시적인 타입 선언은 코드의 의도를 명확히 하고, 컴파일러가 더 나은 에러 메시지를 제공하며, 문서화 역할도 합니다.

개요

간단히 말해서, 함수 시그니처는 매개변수와 반환값의 타입을 명시적으로 선언해야 하지만, 함수 본문 내부의 변수는 타입 추론이 가능합니다. 함수 시그니처의 명시성은 API 설계의 핵심입니다.

예를 들어, 라이브러리를 만들 때 함수 시그니처는 사용자가 보는 문서이자 인터페이스입니다. 타입이 명확하면 IDE가 자동 완성을 제공하고, 컴파일러가 잘못된 사용을 미리 잡아낼 수 있습니다.

기존 Python이나 JavaScript 같은 동적 타입 언어에서는 런타임에 타입 에러가 발생했다면, Rust에서는 컴파일 타임에 모든 타입을 검증합니다. 이는 런타임 버그를 크게 줄여주고, 리팩토링을 안전하게 만들어줍니다.

함수 시그니처의 핵심 특징은 명시적 타입 선언, 문서화 역할, 그리고 타입 검사의 경계입니다. 함수 내부는 자유롭게 타입 추론을 사용할 수 있지만, 경계에서는 반드시 타입을 명시해야 합니다.

코드 예제

// 함수 시그니처는 명시적으로 타입 선언
fn process_data(input: Vec<i32>, threshold: i32) -> Vec<i32> {
    // 함수 내부는 타입 추론 가능
    let mut result = Vec::new(); // Vec<i32>로 추론됨

    for value in input {
        // value의 타입은 i32로 추론됨
        if value > threshold {
            let doubled = value * 2; // i32로 추론됨
            result.push(doubled);
        }
    }

    result // Vec<i32> 반환
}

fn main() {
    let numbers = vec![5, 10, 15, 20];
    let filtered = process_data(numbers, 10);
    println!("{:?}", filtered); // [30, 40]
}

설명

이것이 하는 일: 위 코드는 명시적 함수 시그니처와 내부 타입 추론을 조화롭게 사용하여 벡터를 필터링하고 변환하는 함수를 구현합니다. 첫 번째로, process_data 함수의 시그니처는 input: Vec<i32>, threshold: i32, 그리고 -> Vec<i32>를 명시적으로 선언합니다.

이는 "이 함수는 정수 벡터와 임계값을 받아서, 새로운 정수 벡터를 반환한다"는 계약을 명확히 합니다. 함수를 사용하는 사람은 시그니처만 보고도 어떻게 사용해야 하는지 정확히 알 수 있습니다.

이는 특히 팀 프로젝트나 오픈소스에서 중요합니다. 그 다음으로, 함수 본문 내부에서는 타입 추론을 적극 활용합니다.

let mut result = Vec::new()는 타입을 명시하지 않았지만, 나중에 i32 값을 push하므로 컴파일러가 Vec<i32>로 추론합니다. 마찬가지로 doubled 변수도 value * 2의 결과이므로 i32로 자동 추론됩니다.

이런 추론 덕분에 중복된 타입 선언을 피하면서도 타입 안정성을 유지할 수 있습니다. 그리고 for value in input 루프에서 value의 타입도 자동으로 i32로 추론됩니다.

input이 Vec<i32>이므로 이터레이터가 생성하는 각 요소는 i32입니다. Rust의 타입 추론 엔진은 매우 강력하여 이런 복잡한 상황에서도 정확한 타입을 추론할 수 있습니다.

마지막으로, result를 반환할 때도 타입 체크가 이루어집니다. 함수 시그니처는 Vec<i32>를 반환한다고 선언했고, result는 Vec<i32>로 추론되었으므로 타입이 일치하여 정상적으로 컴파일됩니다.

만약 타입이 맞지 않으면 컴파일러가 명확한 에러 메시지를 제공합니다. 여러분이 이 코드를 사용하면 코드가 간결해지면서도 타입 안정성을 유지할 수 있습니다.

함수 시그니처는 명확한 문서 역할을 하고, 내부 구현은 불필요한 타입 선언 없이 깔끔하게 작성할 수 있습니다. 또한 IDE가 더 나은 자동 완성과 타입 정보를 제공할 수 있습니다.

실전 팁

💡 함수 시그니처에서는 절대 타입을 생략하지 마세요. 컴파일 에러가 발생합니다.

💡 함수 내부에서는 타입이 명확할 때 let x = ...처럼 타입을 생략하세요. 코드가 간결해집니다.

💡 복잡한 타입은 타입 별칭(type alias)을 사용하여 가독성을 높이세요. type Result<T> = std::result::Result<T, MyError>처럼요.

💡 제네릭을 사용하면 여러 타입에 대해 작동하는 함수를 만들 수 있습니다. fn process<T>(data: Vec<T>) -> Vec<T>처럼요.

💡 타입 추론이 실패하면 컴파일러가 알려줍니다. 그럴 때만 타입을 명시적으로 추가하세요.


7. 조기 반환 패턴 - return으로 빠져나가기

시작하며

여러분이 함수에서 여러 단계의 검증을 수행할 때, "모든 if를 중첩하면 코드가 너무 깊어져서 읽기 힘들다"는 고민을 해본 적 있나요? 특히 에러 조건을 먼저 체크하고 정상 로직을 나중에 처리하고 싶을 때, 코드 구조가 복잡해집니다.

이런 문제는 실무에서 매우 흔합니다. 입력 검증, 권한 확인, 전제 조건 체크 등을 모두 if-else로 중첩하면 "화살촉 코드(arrow code)" 또는 "지옥의 피라미드(pyramid of doom)"라 불리는 가독성 낮은 코드가 됩니다.

바로 이럴 때 필요한 것이 조기 반환(early return) 패턴입니다. 에러 조건을 먼저 처리하고 빠르게 반환하면, 정상 로직이 중첩 없이 깔끔하게 작성됩니다.

개요

간단히 말해서, 조기 반환은 함수 중간에 return 키워드를 사용하여 즉시 반환하는 패턴입니다. 에러 조건을 먼저 체크하고 빠르게 빠져나가는 "guard clause" 패턴이라고도 합니다.

조기 반환 패턴은 방어적 프로그래밍의 핵심입니다. 예를 들어, 사용자 입력 검증, null 체크, 권한 확인 같은 작업을 함수 상단에서 처리하고, 조건을 만족하지 않으면 즉시 반환합니다.

이렇게 하면 함수의 나머지 부분은 "모든 조건이 만족된 상태"라고 가정할 수 있어 로직이 단순해집니다. 기존에는 if-else를 깊게 중첩했다면, 조기 반환을 사용하면 코드가 선형적으로 흐릅니다.

각 단계마다 "이 조건이 아니면 여기서 끝"이라고 명확히 하여 인지 부하를 줄입니다. 조기 반환의 핵심 특징은 가독성 향상, 중첩 감소, 그리고 명확한 에러 처리입니다.

특히 Result 타입과 ? 연산자를 함께 사용하면 에러 전파가 매우 간결해집니다.

코드 예제

// 나쁜 예: 중첩된 if-else
fn calculate_price_bad(quantity: i32, price: f64) -> Result<f64, String> {
    if quantity > 0 {
        if price > 0.0 {
            Ok(quantity as f64 * price)
        } else {
            Err("가격은 양수여야 합니다".to_string())
        }
    } else {
        Err("수량은 양수여야 합니다".to_string())
    }
}

// 좋은 예: 조기 반환
fn calculate_price_good(quantity: i32, price: f64) -> Result<f64, String> {
    // Guard clauses: 에러 조건을 먼저 체크
    if quantity <= 0 {
        return Err("수량은 양수여야 합니다".to_string());
    }

    if price <= 0.0 {
        return Err("가격은 양수여야 합니다".to_string());
    }

    // 정상 로직은 중첩 없이 작성
    Ok(quantity as f64 * price)
}

fn main() {
    match calculate_price_good(5, 10.0) {
        Ok(total) => println!("총액: {}", total),
        Err(e) => println!("에러: {}", e),
    }
}

설명

이것이 하는 일: 위 코드는 중첩된 if-else와 조기 반환 패턴을 비교하여, 어떻게 조기 반환이 코드를 더 읽기 쉽게 만드는지 보여줍니다. 첫 번째로, calculate_price_bad 함수는 전통적인 중첩 방식입니다.

조건을 하나씩 체크하면서 if 안에 또 if를 넣는 구조로, 정상 로직이 가장 깊은 곳에 묻혀 있습니다. 이런 구조는 조건이 3-4개만 되어도 가독성이 급격히 떨어지고, 각 else가 어떤 if와 짝인지 파악하기 어려워집니다.

또한 새로운 검증 로직을 추가하려면 전체 구조를 변경해야 합니다. 그 다음으로, calculate_price_good 함수는 조기 반환 패턴을 사용합니다.

quantity <= 0 조건을 먼저 체크하고, 조건이 참이면 즉시 에러를 반환합니다. return 키워드를 명시적으로 사용하여 함수를 중단하고 빠져나갑니다.

이를 "가드 절(guard clause)"이라고 부르며, 함수의 나머지 부분은 "이 조건이 이미 통과되었다"고 가정할 수 있습니다. 그리고 price 검증도 마찬가지로 독립적인 if 문으로 처리합니다.

각 검증이 독립적이므로 순서를 바꾸거나 새로운 검증을 추가하기 쉽습니다. 코드가 위에서 아래로 선형적으로 흐르므로 읽는 사람의 인지 부하가 줄어듭니다.

마지막으로, 모든 검증을 통과하면 정상 로직인 Ok(quantity as f64 * price)가 실행됩니다. 이 부분은 중첩 없이 함수의 최상위 레벨에 작성되어 있어 매우 명확합니다.

새로운 개발자가 이 코드를 읽을 때 "어떤 조건에서 에러가 나는지", "정상 로직이 무엇인지"를 빠르게 파악할 수 있습니다. 여러분이 이 패턴을 사용하면 코드 유지보수가 훨씬 쉬워집니다.

버그를 찾을 때도 각 검증 로직이 독립적이므로 문제를 빠르게 격리할 수 있고, 새로운 검증을 추가할 때도 기존 구조를 건드리지 않고 가드 절만 추가하면 됩니다. 또한 단위 테스트를 작성할 때도 각 에러 케이스를 명확히 테스트할 수 있습니다.

실전 팁

💡 에러 조건을 항상 함수 상단에 배치하세요. "먼저 실패 케이스를 처리하고, 나머지는 성공 케이스"라는 원칙입니다.

💡 Result<T, E> 타입과 ? 연산자를 함께 사용하면 더욱 간결해집니다. return Err(...) 대신 Err(...)?를 쓸 수 있습니다.

💡 가드 절은 부정 조건으로 작성하세요. if is_valid 대신 if !is_valid를 사용하여 "틀린 경우 빠져나간다"를 명확히 하세요.

💡 조기 반환이 너무 많으면(5개 이상) 함수를 분리하는 것을 고려하세요. 검증 로직과 비즈니스 로직을 분리하면 좋습니다.

💡 Option 타입에서는 if let Some(value) = ... else { return None; } 패턴을 사용할 수 있습니다.


8. 함수 오버로딩은 없다 - Rust의 선택

시작하며

여러분이 Java나 C++에서 같은 이름의 함수를 매개변수만 다르게 여러 개 정의했던 경험이 있다면, Rust에서 "왜 add(a, b)와 add(a, b, c)를 같은 이름으로 못 만들지?"라는 의문을 가져본 적 있을 겁니다. 이런 차이는 Rust의 설계 철학 때문입니다.

함수 오버로딩(function overloading)은 편리하지만, 암묵적인 타입 변환과 결합되면 코드를 이해하기 어렵게 만들고 예상치 못한 동작을 유발할 수 있습니다. Rust는 명시성을 중요하게 여깁니다.

바로 이럴 때 알아야 할 것이 Rust가 오버로딩을 지원하지 않는 대신 제공하는 대안들입니다. 트레이트, 제네릭, 그리고 명확한 함수명을 통해 더 예측 가능한 코드를 작성할 수 있습니다.

개요

간단히 말해서, Rust는 함수 오버로딩을 지원하지 않습니다. 같은 이름의 함수를 여러 개 정의할 수 없으며, 각 함수는 고유한 이름을 가져야 합니다.

함수 오버로딩이 없는 것은 처음에는 불편해 보이지만, 실제로는 코드를 더 명확하게 만듭니다. 예를 들어, Java에서 parseInt(String)과 parseInt(String, int)가 있으면 어떤 것이 호출되는지 문맥을 파악해야 하지만, Rust에서는 parse_int와 parse_int_with_radix처럼 명확히 구분합니다.

기존 C++이나 Java에서는 오버로딩으로 편의를 제공했다면, Rust에서는 명시적인 이름과 트레이트를 통해 같은 기능을 더 안전하게 구현합니다. 특히 타입 변환이 자동으로 일어나지 않아 의도하지 않은 동작을 방지합니다.

오버로딩 대안의 핵심 특징은 명시적 함수명, 트레이트 기반 다형성, 그리고 제네릭입니다. 이들은 오버로딩보다 더 강력하고 유연한 추상화를 제공합니다.

코드 예제

// 오버로딩 불가: 같은 이름 사용 불가
// fn add(a: i32, b: i32) -> i32 { a + b }
// fn add(a: i32, b: i32, c: i32) -> i32 { a + b + c } // 컴파일 에러!

// 대안 1: 명시적인 이름 사용
fn add_two(a: i32, b: i32) -> i32 {
    a + b
}

fn add_three(a: i32, b: i32, c: i32) -> i32 {
    a + b + c
}

// 대안 2: 제네릭과 트레이트 사용
use std::ops::Add;

fn add_values<T: Add<Output = T>>(a: T, b: T) -> T {
    a + b // i32, f64, String 등 모두 가능
}

// 대안 3: 가변 인자처럼 사용 (슬라이스)
fn add_all(numbers: &[i32]) -> i32 {
    numbers.iter().sum()
}

fn main() {
    println!("{}", add_two(1, 2)); // 3
    println!("{}", add_three(1, 2, 3)); // 6
    println!("{}", add_values(10, 20)); // 30
    println!("{}", add_values(1.5, 2.5)); // 4.0
    println!("{}", add_all(&[1, 2, 3, 4])); // 10
}

설명

이것이 하는 일: 위 코드는 Rust에서 오버로딩 없이 유사한 기능을 구현하는 세 가지 대안을 보여줍니다. 첫 번째로, 명시적인 함수명 사용은 가장 간단한 방법입니다.

add_two와 add_three처럼 함수가 하는 일을 이름에 명확히 나타냅니다. 이 방법의 장점은 함수를 호출할 때 정확히 어떤 동작을 하는지 이름만 보고 알 수 있다는 것입니다.

단점은 비슷한 함수가 많아지면 네이밍이 어려워질 수 있다는 점이지만, 실무에서는 오히려 이것이 코드 리뷰와 유지보수에 도움이 됩니다. 그 다음으로, 제네릭과 트레이트를 사용한 add_values 함수는 더 강력한 추상화를 제공합니다.

<T: Add<Output = T>>는 "T 타입은 Add 트레이트를 구현해야 하며, 덧셈 결과도 T 타입이어야 한다"는 의미입니다. 이렇게 하면 i32, f64, String 등 Add 트레이트를 구현한 모든 타입에 대해 작동하는 단일 함수를 만들 수 있습니다.

이는 오버로딩보다 훨씬 더 유연하며, 새로운 타입에 대해서도 자동으로 작동합니다. 그리고 add_all 함수는 슬라이스를 받아 가변 개수의 인자를 처리합니다.

C++의 가변 인자 함수처럼 보이지만, 훨씬 더 안전합니다. 슬라이스는 컴파일 타임에 타입이 검증되고, 길이 정보를 포함하므로 버퍼 오버플로우 같은 문제가 없습니다.

또한 iter().sum()처럼 이터레이터 메서드를 사용하여 간결하게 작성할 수 있습니다. 마지막으로, main 함수에서 각 대안을 호출합니다.

add_values(10, 20)과 add_values(1.5, 2.5)는 같은 함수지만 다른 타입으로 호출되며, 컴파일러가 자동으로 적절한 버전을 생성합니다(monomorphization). 이는 런타임 오버헤드 없이 타입 안정성을 제공하는 Rust의 제로 비용 추상화(zero-cost abstraction) 원칙입니다.

여러분이 이 패턴들을 사용하면 오버로딩보다 더 명확하고 안전한 코드를 작성할 수 있습니다. 명시적 함수명은 코드 가독성을 높이고, 제네릭은 코드 재사용성을 극대화하며, 슬라이스는 유연성과 안전성을 동시에 제공합니다.

또한 IDE가 더 정확한 자동 완성과 타입 정보를 제공할 수 있습니다.

실전 팁

💡 함수명에 타입 정보를 포함하지 마세요. add_i32, add_f64 대신 add_two, add_three처럼 동작을 설명하세요.

💡 3개 이상의 유사한 함수가 생기면 제네릭을 고려하세요. 코드 중복을 크게 줄일 수 있습니다.

💡 트레이트를 사용하면 여러 타입에 대해 같은 인터페이스를 제공할 수 있습니다. 오버로딩보다 강력합니다.

💡 빌더 패턴을 사용하면 선택적 매개변수를 우아하게 처리할 수 있습니다. Config::new().with_timeout(30).build()처럼요.

💡 매크로를 사용하면 비슷한 함수를 자동 생성할 수 있지만, 남용하지 마세요. 가독성이 떨어질 수 있습니다.


#Rust#Function#Parameters#ReturnValue#FunctionSignature#프로그래밍언어

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.