콘텐츠로 이동

Rust 클로저

클로저(closure)는 변수에 담아 두고 여기저기 넘겨줄 수 있는, 이름 없는 작은 함수예요. 일반 함수와 다른 점은 자신이 만들어진 스코프의 변수를 “캡처”할 수 있다는 거예요.

fn main() {
let add_one = |x: i32| x + 1;
println!("{}", add_one(5));
}

실행 결과:

6
▶ 직접 실행해 보기

세로 막대 |x: i32|가 일반 함수의 괄호 역할을 해요. 두 가지를 나란히 비교해 볼게요.

fn add_one_fn(x: i32) -> i32 { x + 1 } // 일반 함수
let add_one_closure = |x: i32| x + 1; // 같은 동작을 하는 클로저

Rust는 매개변수 타입과 반환 타입도 추론할 수 있는 경우가 많아서, 클로저를 이보다 더 짧게 쓴 코드도 자주 볼 수 있어요.

let add_one = |x| x + 1;

매개변수가 없는 클로저는 빈 세로 막대를 써요. let say_hi = || println!("hi");처럼 만들고, say_hi()로 호출해요.

표현식이 둘 이상이라면 함수처럼 본문을 중괄호로 감싸요.

fn main() {
let describe = |n: i32| {
let kind = if n % 2 == 0 { "짝수" } else { "홀수" };
format!("{n}은(는) {kind}예요")
};
println!("{}", describe(7));
}

실행 결과:

7은(는) 홀수예요
▶ 직접 실행해 보기

format!은 println!처럼 동작하지만, 텍스트를 출력하는 대신 String으로 돌려줘요. 여기서는 그 String이 중괄호 안의 마지막 표현식이라서 클로저의 반환값이 돼요.

일반 함수에는 없는 기능이 바로 이거예요. 클로저는 따로 넘겨주지 않아도, 자신이 정의된 곳의 변수를 쓸 수 있어요.

fn main() {
let discount = 10;
let apply_discount = |price: i32| price - discount;
println!("{}", apply_discount(100)); // 90
println!("{}", apply_discount(50)); // 40
}

실행 결과:

90
40
▶ 직접 실행해 보기

discount는 클로저의 매개변수가 아니에요. 그냥 main의 스코프에 있을 뿐인데, 클로저가 손을 뻗어서 직접 써요. 일반 fn은 이렇게 할 수 없어요. discount를 인자로 넘겨받아야 해요.

클로저는 캡처한 변수를 바꿀 수도 있어요. 단, 그 변수가 mut여야 해요. 규칙이 하나 더 있어요. 클로저 자체도 mut 변수에 담아야 해요. 클로저를 호출하면 무언가가 바뀌니까, Rust는 그 사실이 코드에 드러나길 원하거든요.

fn main() {
let mut count = 0;
let mut increment = || {
count += 1;
println!("현재 count: {count}");
};
increment();
increment();
increment();
println!("최종 count: {count}");
}

실행 결과:

현재 count: 1
현재 count: 2
현재 count: 3
최종 count: 3
▶ 직접 실행해 보기

increment에 mut를 빼먹으면 컴파일러가 무엇이 잘못됐는지 정확히 알려 줘요: calling increment requires mutable binding due to mutable borrow of count. “count를 가변으로 대여하고 있으니, increment를 호출하려면 가변 바인딩이 필요하다”는 뜻이에요.

알아 두면 좋은 함정이 하나 있어요. 클로저를 아직 호출할 수 있는 동안에는 클로저가 count를 바꿀 권리를 갖고 있어서, 다른 누구도 count를 건드릴 수 없어요. 두 번의 increment() 호출 사이에 println!("{count}");를 넣으면 error[E0502]: cannot borrow count as immutable because it is also borrowed as mutable 에러가 나요. “이미 가변으로 대여 중이라서 불변으로 대여할 수 없다”는 뜻이에요. 마지막 increment() 호출이 끝나면 클로저의 할 일도 끝나고, count는 다시 온전히 여러분의 것이 돼요. 그래서 최종 count 줄은 잘 동작해요. 이 규칙의 전체 이야기는 대여 페이지에서 배워요.

때로는 클로저가 캡처한 값의 소유권을 통째로 가져와야 할 때가 있어요. 예를 들어 클로저를 다른 스레드에 넘길 때가 그래요. 그럴 때는 세로 막대 앞에 move를 써요: move || { ... }. 실제로 쓰는 모습은 동시성 페이지에서 볼 수 있어요.

클로저를 단독으로 쓸 일은 많지 않아요. 클로저가 정말 유용한 곳은 다른 함수에 원하는 동작을 넘겨줄 때예요. 특히 반복자 페이지에서 배울 반복자 메서드와 함께 많이 써요.

fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let doubled: Vec<i32> = numbers.iter().map(|n| n * 2).collect();
println!("{:?}", doubled);
}

실행 결과:

[2, 4, 6, 8, 10]
▶ 직접 실행해 보기

여기서 |n| n * 2는 .map()에 바로 넘겨준 클로저예요. 이런 패턴(작은 클로저가 .map()이나 .filter() 같은 메서드를 움직이는 것)이 매일 실제로 클로저를 쓰게 되는 곳이에요.

tax_percent 변수를 20으로 만드세요. 그다음 가격을 받아서, 캡처한 tax_percent를 이용해 세금을 더한 가격을 돌려주는 클로저 with_tax를 작성하세요. 가격 50과 120에 대한 결과를 출력해 보세요. 힌트: 세금은 price * tax_percent / 100이에요.

정답 보기
fn main() {
let tax_percent = 20;
let with_tax = |price: i32| price + price * tax_percent / 100;
println!("50원에 세금을 더하면 {}원", with_tax(50));
println!("120원에 세금을 더하면 {}원", with_tax(120));
}

실행 결과:

50원에 세금을 더하면 60원
120원에 세금을 더하면 144원
▶ 직접 실행해 보기

tax_percent는 한 번도 넘겨주지 않았어요. 클로저가 main에서 캡처한 거예요. 바로 일반 fn이 할 수 없는 일이죠.

처음 호출하면 1, 두 번째에는 2, 세 번째에는 3을 돌려주는 식으로 동작하는 클로저 next_id를 작성하세요. 세 번 호출해서 각 ID를 출력해 보세요. 힌트: 클로저 바깥에 mut 카운터를 두고, 안에서 1을 더한 다음, 카운터를 클로저의 마지막 표현식으로 두면 그 값이 반환돼요.

정답 보기
fn main() {
let mut last_id = 0;
let mut next_id = || {
last_id += 1;
last_id
};
println!("첫 번째 사용자의 ID: {}", next_id());
println!("두 번째 사용자의 ID: {}", next_id());
println!("세 번째 사용자의 ID: {}", next_id());
}

실행 결과:

첫 번째 사용자의 ID: 1
두 번째 사용자의 ID: 2
세 번째 사용자의 ID: 3
▶ 직접 실행해 보기

last_id와 next_id 둘 다 mut가 필요해요. last_id는 값이 바뀌기 때문이고, next_id는 클로저를 호출하면 캡처한 값이 바뀌기 때문이에요.

  • 클로저는 이름 없는 작은 함수예요: |매개변수| 표현식.
  • 타입은 추론되는 경우가 많아서, 클로저는 보통 같은 동작을 하는 fn보다 짧아요.
  • 일반 함수와 달리, 클로저는 자신이 정의된 스코프의 변수를 캡처할 수 있어요.
  • 캡처한 변수를 바꾸는 클로저는 mut 변수에 담아야 하고, 클로저를 쓰는 동안에는 그 변수를 대여하고 있어요.
  • 클로저는 다른 함수에 넘겨줄 때 가장 유용해요. 특히 .map() 같은 반복자 메서드와 함께요.

이해도 점검

1. 클로저는 매개변수를 쓸 때 함수의 괄호 대신 어떤 기호를 쓸까요?

2. 일반 fn은 못 하는데 클로저는 할 수 있는 일은 무엇일까요?

3. 이 페이지에 따르면 실제로 클로저는 주로 어디에 쓰일까요?

4. 클로저가 캡처한 변수에 count += 1을 해요. 그 밖에 무엇이 필요할까요?

점수: 0 / 4