러스트에서 클로저(Closure)의 개념

러스트의 클로저(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클로저의 호출 방식에 따른 트레잇 분류