> [!abstract] Introduction > 러스트에서는 마치 어떤 값을 다루는 것처럼 함수를 변수에 저장하거나, 다른 함수에 인자로 전달할 수도 있습니다. 이번 글에서는 값처럼 다루어지는 익명 함수, 클로저*closure*에 대해 알아봅니다. ## 논리를 저장하다 ```rust // unwrap_or_else를 사용하면 Result가 Ok일 때 Ok 안의 값을 반환하고, // Err일 때 클로저 안의 코드를 호출한다. let config = Config::build(&args).unwrap_or_else(|err| { eprintln!("Problem parsing arguments: {err}"); process::exit(1); }); ``` 위에 주어진 예시 코드에서 `unwrap_or_else` 메서드 안에 클로저 표현식이 사용되었는데, 이 메서드를 호출한 인스턴스가 의도된 타입이 아닐 때 인자로 주어진 클로저를 실행하게 된다. 사실 클로저 표현식은 [[함수(Rust)|함수]]와 유사한데, `fn`과 소괄호`()`를 쓰지 않는 대신 `||` 사이에 매개변수가 들어가고 중괄호를 통해 함수 본문을 정의하면 된다. 그럼에도 불구하고 클로저가 함수와 동떨어져 보이는 것은 일반적으로 클로저를 작성할 때 매개변수와 반환 값의 타입을 명시하지 않고 때에 따라서는 중괄호`{}`도 생략하기 때문인데, 생략된 코드를 추가하면 다음과 같다. ```rust let config = Config::build(&args).unwrap_or_else(|err: &str| -> () { eprintln!("Problem parsing arguments: {err}"); process::exit(1); }); ``` 이렇듯 클로저에서 사용되는 매개변수와 반환 값의 타입은 컴파일러가 알아서 추론하기 때문에, 같은 클로저를 여러 번 사용할 때 처음과 다른 타입의 인자를 넣으면 에러가 발생한다. 컴파일러는 클로저가 처음 호출될 때 타입을 확정하고, 이후 호출에서는 그 타입을 강제한다. ```rust let example_closure = |x| x; let s = example_closure(String::from("hello")); let n = example_closure(5); // 에러: String 타입이 확정되었으므로 정수를 넣을 수 없다 ``` ``` error[E0308]: mismatched types --> src/main.rs:5:29 | 5 | let n = example_closure(5); | --------------- ^- help: try using a conversion method: `.to_string()` | | | | | expected struct `String`, found integer | arguments to this function are incorrect ``` 여기까지만 보면 클로저는 이름 없는 함수에 불과해 보인다. 하지만 클로저에는 일반 함수와 근본적으로 다른 특성이 하나 있다. ## 환경 캡처 클로저가 일반 함수와 구분되는 핵심적인 차이점은 클로저가 정의된 환경*environment*에 있는 값을 함께 포착*capture*할 수 있다는 것이다.[^environment-capture] 일반 함수는 자신의 매개변수와 지역 변수만 사용할 수 있지만, 클로저는 자신이 정의된 범위에 있는 변수를 가져와서 사용할 수 있다. ```rust fn main() { let list = vec![1, 2, 3]; println!("클로저 정의 전: {:?}", list); let only_borrows = || println!("클로저 내부에서: {:?}", list); println!("클로저 호출 전: {:?}", list); only_borrows(); println!("클로저 호출 후: {:?}", list); } ``` 이 예시에서 `only_borrows` 클로저는 매개변수가 없지만, 바깥 스코프에 있는 `list`를 사용하고 있다. 클로저가 `list`를 읽기만 하므로 불변 참조로 캡처하며, 클로저 정의 이후에도 `list`를 계속 사용할 수 있다. 이러한 환경 캡처는 [[소유권|소유권]] 시스템과 밀접하게 연결되어 있는데, 클로저가 캡처한 값을 어떻게 사용하느냐에 따라 캡처 방식이 달라지기 때문이다. [^environment-capture]: 환경 캡처란, 클로저가 정의된 렉시컬 스코프*lexical scope*에 존재하는 변수의 값을 클로저 내부로 가져오는 것을 말한다. 이 개념은 함수형 프로그래밍에서 비롯되었으며, JavaScript나 Python 등 다른 언어의 클로저에서도 동일한 원리가 적용된다. ### 캡처의 세 가지 방식 클로저가 환경의 값을 캡처하는 방식은 [[소유권#참조와 대여|참조와 대여]]의 규칙을 그대로 따른다. 러스트 컴파일러는 클로저 본문에서 캡처한 값을 어떻게 사용하는지 분석하여, 가능한 한 가장 제약이 적은 방식을 자동으로 선택한다. **불변 대여**로 캡처하는 경우, 클로저는 캡처한 값을 읽기만 한다. 앞서 본 `only_borrows` 클로저가 이에 해당한다. 클로저가 살아있는 동안 원본 변수에 대한 불변 참조가 유지되므로, 클로저 정의와 마지막 호출 사이에서도 원본 변수를 읽을 수 있다. **가변 대여**로 캡처하는 경우, 클로저는 캡처한 값을 변경한다. ```rust fn main() { let mut list = vec![1, 2, 3]; println!("클로저 정의 전: {:?}", list); let mut borrows_mutably = || list.push(7); // 여기서 println!("{:?}", list)를 사용하면 컴파일 에러가 발생한다. // 가변 대여가 활성화된 동안 불변 대여를 할 수 없기 때문이다. borrows_mutably(); println!("클로저 호출 후: {:?}", list); } ``` `borrows_mutably` 클로저는 `list`에 값을 추가하므로 가변 참조로 캡처한다. 여기서 주목할 점은 [[소유권#가변 참조자|가변 참조자의 규칙]]이 그대로 적용된다는 것이다. 클로저가 가변 참조를 쥐고 있는 동안에는 다른 참조가 존재할 수 없으므로, 클로저 정의와 마지막 호출 사이에서 `list`를 사용하면 컴파일 에러가 발생한다. 클로저의 마지막 호출이 끝나면 가변 대여가 해제되어 다시 `list`를 자유롭게 사용할 수 있다. **소유권 이전**으로 캡처하는 경우, 클로저가 값의 소유권을 완전히 가져간다. 이 경우 원본 변수는 더 이상 사용할 수 없다. ### `move` 키워드 클로저가 캡처한 값을 읽기만 하더라도 소유권을 강제로 이전시켜야 하는 상황이 있다. 대표적인 경우가 클로저를 다른 스레드*thread*로 보낼 때인데, 이때는 `move` 키워드를 사용한다. ```rust use std::thread; fn main() { let list = vec![1, 2, 3]; println!("클로저 정의 전: {:?}", list); thread::spawn(move || println!("스레드에서: {:?}", list)) .join() .unwrap(); } ``` `move` 키워드를 `||` 앞에 붙이면, 클로저 본문에서 캡처한 값을 어떻게 사용하든 상관없이 소유권이 클로저로 이전된다.[^move-thread] 이 예시에서 `list`는 읽기만 하지만, 새로 생성된 스레드가 메인 스레드보다 먼저 끝날지 나중에 끝날지 알 수 없으므로 참조자의 유효성을 보장할 수 없다. 따라서 `move`로 소유권 자체를 넘겨야 한다. 만약 `move`를 빼면, 클로저가 `list`의 불변 참조를 캡처하는데 그 참조자가 다른 스레드에서 유효하다는 보장이 없으므로 컴파일러가 에러를 발생시킨다. [^move-thread]: `move`는 모든 캡처 변수의 소유권을 이전시킨다. 만약 `i32`처럼 `Copy` [[Trait|트레이트]]를 구현한 타입이라면 소유권 이전 대신 값의 복사가 이루어진다. ## `Fn` 트레이트 클로저가 환경을 캡처하는 방식은 곧 클로저가 어떤 [[Trait|트레이트]]를 구현하느냐를 결정한다. 러스트는 클로저의 동작 방식을 세 가지 `Fn` 트레이트로 분류하며, 모든 클로저는 이 중 하나 이상을 자동으로 구현한다.[^fn-trait-auto] [^fn-trait-auto]: 컴파일러가 클로저 본문을 분석하여 캡처 방식을 결정하고, 그에 맞는 트레이트를 자동으로 구현한다. 개발자가 직접 `Fn` 트레이트를 구현할 필요는 없다. ### `FnOnce` `FnOnce`는 캡처한 값의 소유권을 클로저 밖으로 이동시키는 클로저에 구현된다. 이름에 *Once*가 붙은 이유는 소유권을 이동시키면 그 값을 다시 사용할 수 없으므로, 이 클로저는 한 번만 호출할 수 있기 때문이다. ```rust impl<T> Option<T> { pub fn unwrap_or_else<F>(self, f: F) -> T where F: FnOnce() -> T { match self { Some(x) => x, None => f(), } } } ``` `unwrap_or_else`의 트레이트 바운드가 `FnOnce() -> T`인 이유는 이 메서드가 클로저를 최대 한 번만 호출하기 때문이다. `Option`이 `Some`이면 클로저를 아예 호출하지 않고, `None`일 때만 한 번 호출한다. 모든 클로저는 최소한 `FnOnce`를 구현한다. 한 번은 호출할 수 있어야 클로저로서 의미가 있기 때문이다. ### `FnMut` `FnMut`는 캡처한 값을 밖으로 이동시키지 않으면서 값을 변경하는 클로저에 구현된다. 여러 번 호출할 수 있다. ```rust fn main() { let mut list = vec![1, 2, 3]; println!("정렬 전: {:?}", list); list.sort_by_key(|r| r.abs()); println!("정렬 후: {:?}", list); } ``` `sort_by_key`는 각 요소마다 클로저를 호출하므로 여러 번 호출이 가능해야 하며, 트레이트 바운드로 `FnMut`를 요구한다. 만약 클로저 내부에서 캡처한 값을 밖으로 이동시키려 하면 `FnMut`를 구현할 수 없으므로 컴파일 에러가 발생한다. ```rust fn main() { let mut list = vec![ String::from("hello"), String::from("world"), ]; let mut sort_operations = vec![]; let value = String::from("by key called"); // 이 클로저는 value의 소유권을 sort_operations로 이동시키므로 // FnOnce만 구현하며, FnMut가 필요한 sort_by_key에서는 사용할 수 없다. list.sort_by_key(|r| { sort_operations.push(value); // 에러! r.clone() }); } ``` 이 코드에서 `value`는 `push`에 의해 `sort_operations`로 소유권이 이동하기 때문에 클로저는 `FnOnce`만 구현한다. `sort_by_key`는 `FnMut`를 요구하므로 컴파일이 실패한다. ### `Fn` `Fn`은 캡처한 값을 이동시키지도, 변경하지도 않는 클로저에 구현된다. 환경에서 아무것도 캡처하지 않는 클로저도 여기에 해당한다. `Fn`을 구현하는 클로저는 부작용*side effect* 없이 여러 번 동시에 호출할 수 있어서, 동시성*concurrency* 환경에서 특히 유용하다. ```rust fn main() { let list = vec![1, 2, 3]; // Fn을 구현하는 클로저: list를 불변 참조로만 캡처 let print_list = || println!("{:?}", list); print_list(); print_list(); // 여러 번 호출 가능 } ``` ### 트레이트 계층 세 트레이트는 다음과 같은 포함 관계를 형성한다. ``` FnOnce ⊇ FnMut ⊇ Fn ``` `Fn`을 구현하는 클로저는 자동으로 `FnMut`와 `FnOnce`도 구현하고, `FnMut`를 구현하는 클로저는 자동으로 `FnOnce`도 구현한다.[^fn-hierarchy] 따라서 `FnOnce`를 요구하는 자리에 `Fn` 클로저를 넣을 수 있지만, 반대로 `Fn`을 요구하는 자리에 `FnOnce` 클로저를 넣을 수는 없다. [^fn-hierarchy]: 이 계층 관계는 직관적으로도 이해할 수 있다. 값을 변경하지 않는 클로저(`Fn`)는 당연히 값을 변경하는 자리(`FnMut`)에서도 안전하게 작동하며, 값을 변경하는 클로저는 한 번만 호출하는 자리(`FnOnce`)에서도 문제없이 작동한다. ## 클로저를 매개변수로 받기 클로저를 함수의 매개변수로 받을 때는 [[Generic|제네릭]]과 트레이트 바운드를 사용한다. 아래 함수는 `Fn` 트레이트를 구현하는 어떤 클로저든 받을 수 있다. ```rust fn apply_to_3<F>(f: F) -> i32 where F: Fn(i32) -> i32, { f(3) } fn main() { let double = |x| x * 2; println!("{}", apply_to_3(double)); // 6 } ``` 여기서 `F: Fn(i32) -> i32`는 "`F`는 `i32`를 받아 `i32`를 반환하는 클로저"라는 뜻이다. 필요에 따라 `Fn` 대신 `FnMut`나 `FnOnce`를 사용하면 된다. 어떤 트레이트 바운드를 선택할지는 클로저를 함수 내부에서 어떻게 사용하느냐에 따라 달라진다. ## 클로저를 반환하기 함수에서 클로저를 반환할 때는 `impl Fn` 문법을 사용한다.[^impl-trait] ```rust fn make_adder(x: i32) -> impl Fn(i32) -> i32 { move |y| x + y } fn main() { let add_five = make_adder(5); println!("{}", add_five(3)); // 8 } ``` `make_adder`는 `x`를 캡처한 클로저를 반환한다. 함수가 끝나면 `x`는 스코프를 벗어나므로 `move`를 사용하여 소유권을 클로저로 이전해야 한다. 반환 타입에 `impl Fn(i32) -> i32`를 쓰면 "이 함수는 `Fn(i32) -> i32`를 구현하는 어떤 타입을 반환한다"는 의미가 된다. [^impl-trait]: `impl Trait` 문법은 [[Generic|제네릭]]의 한 형태로, 호출자가 구체적인 타입을 알 필요 없이 트레이트만 만족하면 되는 상황에서 사용한다. 클로저는 각각 고유한 익명 타입을 가지므로 구체적인 타입 이름을 쓸 수 없어 이 문법이 필수적이다. ## 함수 포인터와의 차이 [[함수(Rust)#함수 포인터|함수 포인터]] `fn`과 클로저의 `Fn` 트레이트는 비슷해 보이지만 근본적으로 다르다. `fn`은 환경을 캡처하지 않는 함수만 가리킬 수 있는 구체적인 타입이고, `Fn`은 클로저와 함수 모두를 포괄하는 트레이트다. 함수 포인터 `fn`은 세 가지 `Fn` 트레이트를 모두 구현하므로, 클로저를 인자로 받는 함수에 일반 함수를 전달하는 것도 가능하다. ```rust fn add_one(x: i32) -> i32 { x + 1 } fn apply<F: Fn(i32) -> i32>(f: F, val: i32) -> i32 { f(val) } fn main() { let closure = |x| x + 1; println!("{}", apply(closure, 5)); // 클로저 전달: 6 println!("{}", apply(add_one, 5)); // 함수 전달: 6 } ``` 일반적으로는 `Fn` 트레이트 바운드를 사용하는 것이 더 유연한데, 클로저와 일반 함수를 모두 받을 수 있기 때문이다. --- ## 참고 자료 & 더보기 + Klabnik, S. and Nichols, C. (n.d.) *The Rust Programming Language*, [13. 함수형 언어의 특성: 반복자와 클로저](https://doc.rust-kr.org/ch13-00-functional-features.html). + Klabnik, S. and Nichols, C. (n.d.) *The Rust Programming Language*, [19.4. 고급 함수와 클로저](https://doc.rust-kr.org/ch19-05-advanced-functions-and-closures.html). + Klabnik, S. and Nichols, C. (n.d.) *The Rust Programming Language*, [13.1. 클로저: 자신의 환경을 캡처하는 익명 함수](https://doc.rust-kr.org/ch13-01-closures.html).