메인 콘텐츠로 건너뛰기

러스트 클로저 이해하기: 캡처·Fn 트레이트·move까지 한 번에

요약

러스트(Rust)에서 클로저(closure)는 “이름 없는 함수”처럼 보이지만, 실제로는 주변 환경(변수들)까지 함께 들고 다니는 똑똑한 함수입니다. 그래서 코드를 짧게 만들고(간결한 함수 문법), 타입 추론으로 작성 부담도 줄여주죠.

이 글에서는 러스트 클로저의 기본 개념부터 함수와의 차이, 캡처 방식(공유 참조/가변 참조/값), 그리고 헷갈리기 쉬운 Fn, FnMut, FnOnce 트레이트를 “왜 존재하는지” 관점으로 정리합니다. 마지막으로 move 키워드가 스레드와 클로저 반환에서 왜 자주 등장하는지도 실전 예제로 연결해볼게요.

Rust 클로저 기본 개념: “함수 + 주변 기억”의 합체

클로저는 |인자| { 본문 } 형태로 쓰는 익명 함수입니다. 예를 들어 let add_one = |x| x + 1;처럼 만들 수 있어요.

여기서 러스트가 특히 편한 점은, 클로저 인자 타입을 매번 다 쓰지 않아도 “첫 사용 맥락”을 보고 타입을 확정해준다는 겁니다. 다만 한 번 결정된 타입은 그 클로저에 고정됩니다. 즉, 같은 클로저를 어떤 땐 String, 어떤 땐 정수로 쓰는 식은 안 됩니다(한 클로저는 한 타입 세계관만 산다… 같은 느낌).

정리하면, 클로저는 문법이 짧고 타입 추론이 강력해서 “로컬에서 잠깐 쓰는 함수”에 아주 잘 맞습니다.

클로저 vs 함수: 결정적 차이는 “캡처(capture)”

함수 fn은 깔끔합니다. 인자를 받고, 계산하고, 끝. 대신 함수 바깥에 있는 지역 변수를 슬쩍 가져다 쓰는 건 불가능합니다.

클로저는 반대로 “주변 스코프에 있던 변수”를 몰래 주머니에 넣어올 수 있습니다. 이게 캡처(capture)입니다. 컴퓨터과학에서 말하는 클로저도 결국 “함수 코드 + 환경(environment)의 묶음”이라는 설명이 흔하죠.1

이 차이 덕분에 클로저는 콜백, 이터레이터(map/filter), 이벤트 핸들러, 스레드 작업 같은 곳에서 압도적으로 자주 등장합니다.

캡처 방식 3종 세트: 공유 참조·가변 참조·값(소유권)

러스트는 클로저가 주변 변수를 어떻게 쓰는지 분석해서, “필요한 만큼만” 캡처 권한을 줍니다. 즉, 최소 권한 원칙으로 움직여요.

첫 번째는 공유 참조(immutable borrow) 캡처입니다. 클로저가 변수를 읽기만 하면 &T로 빌려서 가져옵니다. 이 경우 클로저를 여러 번 호출해도 안전하고, 외부 값도 그대로 살아 있습니다. “보기만 할게요” 모드죠.

두 번째는 변경 가능한 참조(mutable borrow) 캡처입니다. 클로저가 바깥 변수를 바꾸면 &mut T가 필요합니다. 그래서 이런 클로저는 보통 let mut c = || ...;처럼 클로저 변수 자체도 mut여야 하는 장면이 자주 나옵니다. “내가 값 좀 고칠게요” 모드입니다.

세 번째는 값으로 캡처(ownership move)입니다. 클로저가 바깥 변수의 소유권을 가져가 버리는 경우예요. 예를 들어 클로저 안에서 drop(s)를 호출하거나, s를 다른 곳에 넘겨버리면, s는 클로저로 이사 갑니다. 이때는 클로저가 끝난 뒤 바깥에서 그 변수에 접근할 수 없습니다. “이건 내 거야” 모드죠.

실제로는 우리가 “캡처 방식을 지정”하는 게 아니라, 클로저 본문이 어떤 행동을 하느냐에 따라 컴파일러가 캡처 방식을 결정합니다.

FnOnce · FnMut · Fn: 이름보다 “허용되는 호출 방식”을 보자

러스트에서 클로저는 세 가지 트레이트 중 하나(혹은 여러 개)를 구현합니다. 이 셋은 계단처럼 생각하면 편합니다.

FnOnce는 “최소 조건”입니다. 이름 그대로 한 번은 호출할 수 있어요. 왜 최소냐면, 소유권을 가져가는 클로저도 최소 1번은 호출 가능하니까요. 그래서 모든 클로저는 기본적으로 FnOnce 성질을 갖습니다.

FnMut는 “여러 번 호출 가능하지만, 호출 사이에 내부 상태(캡처한 값)가 바뀔 수 있음”을 허용합니다. 바깥 변수를 &mut로 잡고 있거나, 클로저 자기 내부의 캡처 상태를 수정하면 보통 이쪽으로 갑니다. 중요한 포인트는 “입력 인자를 바꾸느냐”가 아니라 “캡처한 환경을 바꾸느냐”입니다.2

Fn은 가장 빡빡하지만 가장 재사용성이 좋습니다. 캡처한 값을 읽기만 하고(불변), 소유권을 빼앗지도 않으면 Fn이 됩니다. 이런 클로저는 여러 번 호출해도 항상 안전하고 예측 가능하죠.

여기서 실무 팁 하나. IDE나 타입 추론 메시지를 보다 보면 “이 클로저는 FnOnce네요?” 하고 놀랄 때가 있는데, 실제로는 Fn인 클로저도 동시에 FnOnce를 구현합니다. 즉, “FnOnce로도 쓸 수 있다”와 “FnOnce만 된다”를 구분해야 합니다. 러스트 포럼에서도 이 지점에서 혼동이 자주 생깁니다.3

move 키워드: “캡처를 소유로 강제”하는 안전장치

move는 클로저가 주변 변수를 캡처할 때, 빌리는 대신 “소유권을 가져가라”고 강제하는 키워드입니다.

이게 왜 필요할까요? 대표 이유는 두 가지입니다.

첫째, 스레드입니다. 스레드는 현재 함수가 끝난 뒤에도 계속 돌아갈 수 있죠. 그런데 바깥 변수를 참조로만 빌려서 들고 가면, 원래 변수는 함수 종료와 함께 사라질 수 있습니다. 즉, “스레드가 들고 있는 참조가 공중분해”될 위험이 생깁니다. 그래서 스레드로 넘길 땐 보통 thread::spawn(move || { ... }) 패턴을 사용해 소유권을 스레드 쪽으로 넘겨 안전을 확보합니다.4

둘째, 클로저를 반환할 때입니다. 어떤 함수가 클로저를 “리턴”한다는 건, 클로저가 함수 바깥으로 나간다는 뜻입니다. 이때도 마찬가지로 바깥 지역 변수의 참조를 들고 나가면 수명이 꼬입니다. move로 필요한 값들을 클로저가 소유하게 만들면, 반환 이후에도 클로저가 자기 데이터로 안전하게 동작할 수 있습니다.

즉, move는 사용성을 줄이는 키워드가 아니라(“용도 제한”), 오히려 “스코프 밖에서도 안전하게 살 수 있게 만드는 생존 키트”에 가깝습니다.

실전에서 자주 만나는 장면 2가지: 스레드와 콜백(수명)

스레드 예시는 위에서 봤고, 콜백에서도 클로저의 캡처/수명 규칙이 자주 튀어나옵니다.

예를 들어 어떤 라이브러리 함수가 “콜백 클로저가 반환하는 값은 입력에서 빌려온 참조일 수 없다” 같은 제약을 걸 때가 있어요. 러스트 포럼의 regex::replace_all 사례가 딱 이런 종류인데, 콜백이 Captures에서 뽑아낸 &str을 그대로 반환하려다 수명 제약에 걸리고, 결국 String처럼 소유하는 값으로 만들어 반환해야 해결됩니다.5

이런 문제를 만났을 때 핵심 질문은 하나입니다. “내 클로저가 지금 빌린 걸 밖으로 들고 나가려고 하나?” 만약 그렇다면, 대개 해결 방향은 둘 중 하나로 정리됩니다.

하나는 to_owned(), String::from()처럼 소유 형태로 변환해 반환하기입니다.

다른 하나는 애초에 API가 기대하는 트레이트(FnOnce/FnMut/Fn)와 수명 조건에 맞춰 설계를 바꾸는 겁니다.

결국 클로저는 함수보다 자유로운 대신, “캡처한 환경과 수명”을 반드시 계산해야 하는 존재라고 보면 감이 딱 옵니다.

시사점 내용 (핵심 포인트 정리 + 개인적인 생각 또는 실용적 조언)...

러스트 클로저는 한 줄로 요약하면 “함수 문법을 빌린 작은 객체”입니다. 이 객체는 주변 값을 캡처하고, 그 캡처 방식에 따라 Fn, FnMut, FnOnce라는 성격이 정해집니다. 그리고 스레드나 반환처럼 스코프를 넘나드는 순간, move가 안전한 소유권 이전을 책임집니다.

클로저 관련 에러를 빨리 푸는 요령은, 코드가 아니라 “권한(참조/가변/소유)”과 “호출 횟수(Once/Mut/Read-only)”로 상황을 다시 번역해보는 겁니다. 컴파일러 메시지가 개인 공격처럼 느껴지는 날에도, 이 번역만 성공하면 해결책은 꽤 논리적으로 보이기 시작합니다.

참고

1Closure (computer programming) - Wikipedia

2Understanding Closures in Rust (Without Losing Your Mind) | by Deepesh Singh Rathore | Dec, 2025 | Medium

3What is the difference between if let Some(ref x) = y and if let Some(x) = y.as_ref()? - help - The Rust Programming Language Forum

4Memory Safety and Performance: Rust Explained for Developers | by Adekola Olawale | Jan, 2026 | Medium

5How to deal with lifetime in closure? - help - The Rust Programming Language Forum

#러스트#클로저#소유권#수명#Fn 트레이트

이 노트는 요약·비평·학습 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

Tilnote 를 사용해 보세요.

키워드만 입력하면 나만의 학습 노트가 완성돼요.

책이나 강의 없이, AI로 위키 노트를 바로 만들어서 읽으세요.

콘텐츠를 만들 때도 사용해 보세요. AI가 리서치, 정리, 이미지까지 초안을 바로 만들어 드려요.