러스트의 클로저(Closure)는 함수형 프로그래밍(Functional Programming)의 핵심 개념 중 하나로서, 변수에 저장하거나 다른 함수에 인자로 넘길 수 있는 익명 함수이며, 정의된 위치의 외부 변수를 자동으로 사용할 수 있습니다. 이것이 일반 함수와 큰 차이점입니다.
함수형 프로그래밍은 다음과 같은 특성을 중시합니다:
특징 | 설명 |
---|---|
일급 함수 (First-class function) | 함수를 값처럼 변수에 할당하거나 인자로 전달 가능 |
고차 함수 (Higher-order function) | 함수를 인자로 받거나 함수를 반환하는 함수 |
불변성 (Immutability) | 상태를 변경하지 않고 새로운 값을 생성 |
순수 함수 (Pure Function) | 동일한 입력에 대해 항상 동일한 출력을 반환, 부작용 없음 |
👉 클로저는 이러한 특징, 특히 일급 함수와 고차 함수 개념을 구현하는 데 매우 적합합니다.
1. 클로저(Closure) 란?
클로저는 익명 함수로 fn과 함수이름이 없습니다.
변수에 저장하거나 다른 함수에 인자로 넘길 수 있습니다.
fn main() {
let add = |a, b| a + b;
println!("{}", add(2, 3)); // 5
}
- |a, b| a + b는 익명 함수(anonymous function) 형태이며, 입력값 a와 b를 받아서 a + b를 반환합니다.
- 이 클로저는 변수 add에 저장됩니다. 다시 말해 add는 클로저를 가리키는 변수(바인딩된 이름)입니다.
- 따라서, 이후에는 add(2, 3)처럼 add를 통해 클로저를 호출할 수 있습니다.
- println!(“{}”, add(2, 3));은 add(2, 3)을 호출하여 2 + 3을 계산하고 결과는 5입니다. 이 결과를 println!을 사용해 출력합니다.
가. function으로 만들었다면?
아래와 같은 코드가 됩니다. 함수는 입력 값과 반환 형식의 타입을 반드시 명시해야 하는데, 클로저는 타입이 추론되는 점도 다릅니다.
fn add(a:i32,b:i32) -> i32 {
a + b
}
fn main() {
println!("{}", add(2, 3));
}
2. 클로저의 환경 캡처(Capturing the Environment)
클로저는 정의된 위치의 외부 변수를 자동으로 사용할 수 있습니다. 이게 일반 함수와 큰 차이점입니다.
fn main() {
let x = 5;
let add_x = |a| a + x;
println!("{}", add_x(3)); // 8}
- 여기서 x는 클로저 외부에서 정의된 변수지만, 클로저 내부에서 자동으로 캡처(capture) 됩니다.
- 함수에서는 이런 일이 불가능합니다. 다시 말해 x가 함수의 입력 인수로 지정돼야 합니다.
fn add_x(a: i32, x: i32) -> i32 {
a + x
} - 또한 클로저의 인수로 a만 있기때문에 add_x의 입력 값으로 a만 입력하면 되고, x는 입력할 필요가 없습니다.
- 실행 결과는 3 + 5 = 8입니다.

3. 캡처 방식에 따른 트레잇
Rust는 클로저가 외부 변수를 어떻게 사용하는지에 따라 다음 3가지 트레잇 중 하나로 자동 구현됩니다:
트레잇 | 설명 | 예시 |
---|---|---|
Fn | 불변 참조로캡처 (&T) | 읽기만 하는 클로저 |
FnMut | 가변 참조로 캡처 (&mut T) | 외부 값을 수정하는 클로저 |
FnOnce | 값을 소유하여 캡처 (T) | 한 번만 호출 가능한 클로저 |
가. Fn (읽기만 함)
fn main() {
let x = 1;
let closure = |a| a + x;
println!("{}", closure(2)); // 3
}
- x를 읽기만 하므로 Fn 트레잇으로 작동합니다.
- 실행 결과는 2 + 1 = 3입니다.
나. FnMut (수정)
fn main() {
let mut x = 1;
let mut closure = |a| {
x += a;
x
};
println!("{}", closure(2)); // 3
println!("{}", closure(2)); // 5
}
- x += a처럼 외부 값인 x를 수정하므로 FnMut 트레잇이 필요합니다. 내부적으로 사용되고 FnMut는 어디에도 없습니다.
- 한번 실행하면 x가 2 + 1 = 3이 되고, 두번째 closure(2)는 2 + 3 = 5를 반환합니다.
- 당연히 x 변수는 mut(가변 변수)로 선언되었습니다.
다. FnOnce (이동)
fn main() {
let s = String::from("hello");
let closure = move || {
println!("{}", s);
};
closure();
// closure(); // ❌ 두 번 호출하면 에러: FnOnce
}
- let closure = move || {
println!(“{}”, s);
};
이 줄에서 클로저를 정의하고 closure라는 변수에 저장합니다. - move 키워드는 클로저가 외부 변수 s의 소유권을 가져오게 만듭니다.
- 즉, s는 더 이상 main 함수 안에서는 사용할 수 없습니다.
- 클로저 내부에서
s
를 사용하므로 소유권이 클로저로 이동(move) 됩니다. - 이 클로저는 FnOnce 트레잇만 구현됩니다.
이유: s의 소유권을 가져오면, 클로저는 단 한 번만 호출할 수 있기 때문입니다. - s가 외부 변수이므로 ||안에 넣지 않고, 실행문 안에 들어가 있습니다.
4. 클로저 vs 함수 비교 표
항목 | 클로저 (Closure) | 함수 (Function) |
---|---|---|
정의 방식 | `let c = | x |
이름 | 익명 함수(Anonymous) 형태, 변수에 저장 | 명시적으로 이름을 정의 |
타입 명시 여부 | 생략 가능 (` | x |
리턴 타입 | 대부분 추론됨 (→ 생략 가능) | 명시하거나 생략 가능 (하지만 복잡한 경우 명시 권장) |
외부 변수 접근 | 가능 (환경 캡처: by ref, mut ref, move) | 불가능 (오직 인자로만 데이터 전달) |
사용 가능 위치 | 함수 내에서 정의, 변수처럼 전달 | 모듈 내 어디든 정의 가능 |
함수 포인터와의 호환 | fn 타입으로 직접 전달하려면 명시 필요 | 기본적으로 fn 타입 (e.g. fn(i32) -> i32) |
트레잇 구현 | Fn, FnMut, FnOnce 자동 구현됨 | 일반 함수는 Fn 트레잇으로 처리 가능 |
호출 방식 | 변수명으로 호출 (c(3)) | 함수명으로 호출 (c(3)) |
Move 가능 여부 | move 키워드로 명시적 이동 가능 | 소유권 개념 없음 (독립된 스코프) |
유연성 | 매개변수와 환경 캡처를 자유롭게 조합 | 환경 변수와는 독립적 |
실행 성능 | 최적화 후 함수 수준의 성능 가능 | 고정된 바이너리 코드로 일반적으로 더 최적화 |
사용 예 | 일시적인 로직, 고차 함수 인자, iterator | 재사용 가능한 명령 블록, API 정의 |
중괄호 생략 가능 여부 | 단일 표현식이면 생략 가능 | 생략 불가 항상 중괄호 {} 필요 |
5. 요약 정리
항목 | 설명 |
---|---|
클로저 | 외부 환경 캡처 가능한 익명 함수 |
Fn, FnMut, FnOnce | 클로저의 호출 방식에 따른 트레잇 분류 |