명명된 매개변수, 선택 인자, 기본 인자, 함수 오버로딩의 장단점과 Rust에서의 설계 과제를 살펴본다.
2026년 9월 21일
다음은 Rust 코드입니다:
// define a function
fn foo(x: i32, y: i32) -> i32 {
// body elided
}
// call it
let z = foo(5, 6);
프로그래밍 언어 용어로 x와 y를 매개변수라고 하고, 5와 6을 인자라고 합니다.
Rust에는 매개변수와 인자에 관련된 화려한 기능이 많지 않습니다. 하지만 다른 언어에는 있습니다. 예를 들어, 제가 아주 많이 작성했던 또 다른 언어인 Ruby의 함수 하나를 보겠습니다:
# its definition in Rails
def redirect_to(options = {}, response_options = {})
# body elided
end
# you can call it in all of these ways:
# pass a string with a URL
redirect_to "http://www.rubyonrails.org"
# pass an instance of a model to go to it, if post.id = 5 then this might go to
# "/posts/5"
redirect_to @post
# invoke an action directly, might be equivalent to the above in a Posts controller
redirect_to action: "show", id: 5
# redirect to a model's URL, passing a flash message
redirect_to post_url(@post), alert: "Watch it, mister!"
# do the same but also set a specific HTTP code
redirect_to post_url(@post), status: :found
# do both
redirect_to post_url(@post), status: 301, flash: { updated_post_id: @post.id }
저는 예전에 Ruby를 좋아했고, 지금은 Rust를 좋아합니다. 그리고 솔직히 말해, redirect_to와 그 친구들은 제가 Rust가 이런 종류의 화려한 기능을 얻는 데 크게 반대하는 이유입니다. 이 기능들은 아주 간결할 수 있고, 어떤 사람의 눈에는 아름다울 수 있습니다. 하지만 함수로 할 수 있는 모든 일을 파악하기가 정말, 정말 어려워집니다. 결국 누군가 좋은 문서를 작성했기를 바라야 합니다. Rails의 문서는 꽤 좋지만, 모든 라이브러리가 그 수준의 문서를 갖추지는 않을 것입니다.
분명히 해두자면, 여기에는 여러 가지가 한데 묶여 있습니다. 유연한 인자 타입, 옵션 해시, 기본값, 그리고 키워드처럼 보이는 문법입니다. 제 회의론을 형성한 것은 인자 레이블만이 아니라 이 API 전체 스타일입니다.
그렇다면 이 기능들은 정확히 무엇일까요?
가장 단순한 것은 명명된 매개변수입니다. 짐작할 수 있듯이, 호출 위치에서 매개변수 이름을 명시적으로 사용할 수 있다는 뜻입니다:
// without named parameters
let z = foo(5, 6);
// if Rust had them
let z = foo(x: 5, y: 6);
명명된 매개변수는 호출 위치에서 더 많은 정보를 제공할 수 있어서 좋지만, 더 유연할 수도 있습니다. 일부 언어에서는 명명된 인자를 다른 순서로 제공할 수도 있습니다:
// since we have names, we can write this:
let z = foo(x: 5, y: 6);
// but we can also write this:
let z = foo(y: 6, x: 5);
명명된 매개변수의 단점은 꽤 장황해질 수 있다는 점입니다:
# FastAPI in Python, UserOut class not shown
@app.get(
"/me",
response_model=UserOut,
status_code=200,
tags=["Users"],
summary="Get current user profile"
)
def get_current_user():
극단적인 예시를 들어 허수아비를 세우지 않으려 했지만, get은 서로 다른 키워드 인자 23개를 받을 수 있으므로 이 호출은 상당히, 상당히 길어질 수 있습니다.
명명된 인자는 표현식을 전달할 때 좋을 수 있습니다. 호출 위치에서 무언가를 더 이해할 수 있기 때문입니다. 위의 status_code=200은 값만 덩그러니 있는 200보다 훨씬 낫습니다. HTTP를 잘 안다면 무엇인지 어느 정도 자명하지만, 추가적인 명확성은 좋습니다. 그러나 정말 복잡한 예시는 흔히 변수로 분해한 뒤 그 변수를 함수에 그대로 전달하게 되며, 그러면 장황하면서도 중복적이 됩니다. 모든 옵션을 사용해 get을 직접 호출하는 FastAPI 내부의 예시를 보겠습니다:
self.router.get(
path,
response_model=response_model,
status_code=status_code,
tags=tags,
dependencies=dependencies,
summary=summary,
description=description,
response_description=response_description,
responses=responses,
deprecated=deprecated,
operation_id=operation_id,
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias,
response_model_exclude_unset=response_model_exclude_unset,
response_model_exclude_defaults=response_model_exclude_defaults,
response_model_exclude_none=response_model_exclude_none,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
callbacks=callbacks,
openapi_extra=openapi_extra,
generate_unique_id_function=generate_unique_id_function,
)
실제로 이 예시에는 또 다른 두 기능이 슬쩍 들어가 있습니다. 매개변수가 23개인 함수에 어떻게 인자 다섯 개만 전달할 수 있었을까요?
이름이 뜻하듯, 선택 인자는 인자를 전달하지 않도록 선택할 수 있는 기능이며, 기본 인자는 인자가 전달되지 않았을 때 사용할 기본값을 설정할 수 있게 해줍니다.
앞의 예시로 돌아가 보겠습니다:
@app.get(
"/me",
response_model=UserOut,
status_code=200,
tags=["Users"],
summary="Get current user profile"
)
def get_current_user():
가능한 인자 22개 중 다섯 개만 전달했습니다. 매번 22개를 전부 전달해야 했다면 엄청나게 장황했을 것입니다. 따라서 일부 매개변수에 기본값을 선언하면 호출 위치에서 이를 생략하고, 인자를 전달하지 않은 대신 기본값을 받을 수 있습니다. Ruby에서도 이를 보았습니다:
# its definition in Rails
def redirect_to(options = {}, response_options = {})
= {}는 options와 response_options의 기본값입니다. 빈 해시 맵입니다.
선택 인자와 기본 인자는 흔히 함께 등장합니다. 결국 인자를 전달하지 않는다면 그 매개변수의 값은 무엇이어야 할까요? 언어에 어떤 종류의 널 값이 있다면 그것이 한 해결책이 될 수 있고, 기본 인자가 없는 선택 인자처럼 느껴질 수 있습니다.
하지만 다음 기능인 함수 오버로딩을 사용하면 진정으로 한쪽만 가질 수 있습니다:
함수 오버로딩을 지원하는 언어에서는 같은 이름이지만 서로 다른 시그니처를 가진 함수를 여러 개 정의할 수 있습니다. 어떤 함수가 호출되는지는 어떤 인자를 전달하는지에 따라 달라집니다. 이를 통해 기본 인자 없이도 진정한 선택 인자를 가질 수 있습니다. Java에서는 다음과 같습니다:
// Version 1: Requires both arguments
void connect(String url, int timeout) { ... }
// Version 2: Timeout is optional to the caller, and handled internally
void connect(String url) { ... }
여기서는 타임아웃을 전달하면 첫 번째 함수를 사용하고, 전달하지 않으면 두 번째 함수를 사용합니다. 널을 쓰는 대신, 해당 매개변수는 아예 존재하지 않습니다.
이는 “함수 디스패치”라고 하는 것의 특정한 형태입니다. 기본적으로 “함수가 호출될 때 정확히 무엇이 호출되는가?”라는 뜻입니다. 이를 수행하는 방법은 아주 많습니다. 지금 이 글에서 그 모든 것을 다루고 싶지는 않지만, 가능성은 무수히 많습니다. 정적 대 동적, 단일 대 다중, 조건자 디스패치, 패턴 매칭 디스패치, 프로토타입 기반 디스패치 등입니다. 언젠가 이에 관한 글도 쓸지 모르겠습니다.
그렇다면 여기서 장단점은 무엇일까요?
위에서 말했듯이, Rust는 현재 이 세 가지 기능을 어느 것도 지원하지 않습니다. 그리고 이 기능들은 수년간 커뮤니티의 꾸준한 요청이었습니다. 이 지원에 관한 12년 된 GitHub 이슈가 여기에 있으며, 심지어 우리가 이야기하지 않은 “가변 길이 인자 목록”도 포함합니다.
이 점이 제가 이 기능들에 대해 싫어하는 첫 번째 이유로 이어집니다. 기능이 너무 많습니다. 그리고 모두 서로 얽혀 있습니다. 앞서 언급했듯이 선택 인자가 있다면 아마 기본 인자도 원할 것입니다. 명명된 매개변수가 있다면 오직 명명된 매개변수만 지원하고 싶은가요, 아니면 둘 다 지원하고 싶은가요? 이들을 그냥 전부 지원하고 싶은 유혹이 매우 크고, 그러면 거대한 복잡성의 산을 추가하게 됩니다.
현재 Rust에서는 이름이 약간 다른 여러 함수를 작성해야 합니다:
// a new empty vector
let v = Vec::new();
// one with a set capacity
let v = Vec::with_capacity(5);
“함수에 서로 다른 이름 두 개를 작성한다”는 단순한 비용으로 규칙을 아주 단순하게 유지할 수 있습니다. 함수 정의는 하나이고, 이름으로 호출합니다. 끝입니다. 다음을 모두 말할 수 있으면 좋을까요?
let v = Vec::new();
let v = Vec::new(5);
let v = Vec::new(capacity: 5);
물론, 그럴지도 모릅니다. 하지만 제게는 비용이 매우, 매우 커 보입니다. 이런 기능에 익숙하다면 그렇게 커 보이지 않을 수도 있지만, 제 몸에는 Ruby 문신이 있습니다. 별일을 다 봤습니다. 그래서 이 영역에서 Rust의 단순함을 즐기게 되었습니다. 맞습니다. 매개변수가 엄청나게 많은 함수가 있다면 대신 빌더를 작성해야 할 수 있고, 거기에도 나름의 장황함이 따릅니다:
// if Vec had a builder
let v = Vec::new()
.with_capacity(5)
.build();
// all of the code you have to write to make the builder elided
하지만 언어 자체는 단순하게 유지되고 규칙도 쉽습니다. 이미 복잡하다고 인식되는 언어에 대해서는, 이것이 언제나 제게 옳게 느껴졌습니다. 그리고 이것이 제가 지난 여러 해 동안 이런 방식으로 Rust를 확장하려는 다양한 제안에 반대해 온 이유입니다.
하지만 최근 이 기능들 가운데 딱 하나에 대해서는 생각을 바꾸었고, Rust가 이를 얻어도 괜찮다고 생각하게 되었습니다. 어쩌면요. 또 중요한 점은, 저는 수년간 Rust 작업을 하지 않았으므로 제 의견은 다소 무의미하다는 것입니다. 그래도 뭐, 제 블로그니까요. 의견은 이런 데 쓰라고 있는 것입니다.
Rust는 명명된 매개변수를 지원해도 괜찮을 것 같습니다. 하지만 선택 매개변수나 기본 매개변수는 아닙니다. 그리고 제 생각을 바꾼 것은 사실 코딩 에이전트입니다. 죄송합니다. 제가 AI 이야기를 하는 것에 지쳤을 수도 있지만, 제 말을 들어 보세요.
위에서 언급했듯이, 명명된 매개변수에 대한 제 불만 중 하나는 장황함입니다. 하지만 그 장황함으로 얻는 것은 명확성입니다. 장황함과 명확함이 항상 같은 것이라고는 생각하지 않습니다. 저는 do/end보다 {}를 좋아하고, 개인적으로 더 읽기 쉽다고 생각합니다. 하지만 image 크레이트의 이 함수를 보겠습니다:
// definition
pub fn crop_imm<I: GenericImageView>(
image: &I,
x: u32,
y: u32,
width: u32,
height: u32,
) -> SubImage<&I> { ... }
// calling it
let cropped = image::imageops::crop_imm(&img, 10, 20, 200, 100);
// if we had named arguments
let cropped = image::imageops::crop_imm(
image: &img,
x: 10,
y: 20,
width: 200,
height: 100,
);
이것은 의심할 여지 없이 가독성을 크게 높입니다. 하지만 저에게는 이 모든 것을 직접 타이핑하는 일이 그다지 큰 보상으로 느껴지지 않습니다. 하지만 Claude가 작성한다면요? 저는 훨씬 덜 신경 씁니다. 그리고 인간에게 주는 가독성 이점은 에이전트에게는 더욱 강력합니다. 인라인으로 쓰인 텍스트의 추가 명확성은 실제 호출 위치를 볼 때 더 도움이 되고 토큰과 호출을 덜 사용하는 듯합니다(아직 이에 대한 제대로 된 평가를 수행하지는 않았습니다…). 에이전트가 위의 명명된 예시를 읽는다면, 함수 시그니처 자체를 찾아보지 않고도 10이 x 값이라는 것을 알 수 있습니다. “인간에게 좋은 것은 에이전트에게도 참이다”가 또 한 번 적용됩니다.
이것이 제가 여전히 선택 인자에 반대하는 이유이기도 합니다. 선택 인자는 함수에 무엇이 전달되는지를 의도적으로 숨기며, 호출 위치의 명확성을 흐립니다. 좋은 점은 타이핑할 양이 줄어든다는 것입니다. 하지만 저는 더 이상 직접 타이핑하지 않습니다. 따라서 이점은 사라지고 단점만 남습니다. 그렇다고 해서 FastAPI 예시에서 foo=foo를 전달하는 장황함 문제가 해결되는 것도 아닙니다. 여전히 중복적입니다. 하지만 단점의 한 부분은 완화되었습니다.
특히 이 모든 논리는 빌더 패턴에도 적용되며, 이것이 제가 Rust에서 빌더를 아껴 쓰려 하는 이유입니다. 빌더는 명명된 인자를 흉내 낼 수 있는데, 그것은 좋습니다. 하지만 선택/기본/가변 인자도 흉내 낼 수 있는데, 그것은 나쁩니다. 때로는 들인 노력만큼 가치가 있지만, 항상 그런 것은 아니며 아마 대다수의 경우에도 그렇지 않을 것입니다.
그러니 가장 큰 단점은 완화하면서 가장 큰 장점은 유지할 수 있습니다. 좋아 보입니다. 하지만 이 기능이 마음에 든다고 해도, Rust에 특히 적용되는 실무적 우려가 여전히 많아서 언어에 추가하는 일이 명백한 선택은 아닙니다.
이제 “이 기능이 추상적으로 좋은가?”와 “이 기능을 구현해야 하는가?”는 매우 다릅니다. 명명된 매개변수를 구현하는 데에는 해결되지 않은 질문과 단점이 아주 많습니다. 단순한 문제 하나는 이전의 모든 함수가 사라지는 것이 아니라는 점입니다. with_capacity는 영원히 존재할 것입니다. 이는 더 어려운 문제들에 비하면 비교적 작은 단점입니다.
먼저, 엄밀히 말해 Rust의 매개변수는 이름이 아니라 패턴입니다. 이름은 단순한 패턴이지만, 더 복잡하게 만들 수도 있습니다:
fn foo((x, y): (i32, i32)) {
이 매개변수에는 이름이 없습니다. 두 바인딩을 도입하는 패턴입니다. 외부 이름과 내부 이름을 구분해 부여하는 문법을 생각해 낼 수는 있겠지만, 복잡성이 또다시 고개를 내밉니다.
함수가 값이 되면 어떻게 될까요? 오늘도 다음을 작성할 수 있습니다:
fn resize(width: u32, height: u32) {
// body elided
}
fn offset(dx: u32, dy: u32) {
// body elided
}
let f: fn(u32, u32) = if resizing { resize } else { offset };
f의 매개변수에는 어떤 이름을 쓸까요? 서로 다른 이름을 가질 수 있습니다. 이것은 오류가 되어야 할까요? f를 호출할 때 어떤 이름을 사용할 수 있을까요? 더 복잡하게 만드는 점은, Rust가 현재 실제로 다음 코드를 받아들인다는 것입니다:
type Callback = fn(width: u32, height: u32);
fn f(g: Callback) {}
fn bar(x: u32, y: u32) {}
fn main() {
f(bar) ;
}
보시다시피 Callback의 이름은 문서화일 뿐이며, 필수는 아닙니다. 이것은 오류가 되어야 할까요? bar가 이름을 바꾸도록 요구해야 할까요? 사실 트레이트 정의도 마찬가지입니다:
trait Writer {
fn write(&mut self, data: &[u8]);
}
struct Sink;
impl Writer for Sink {
fn write(&mut self, bytes: &[u8]) {
// body elided
}
}
이것은 오류일까요? 괜찮을까요?
또 다른 문제가 있습니다. 평가 순서입니다.
fn consume(data: Vec<u8>, length: usize) {
// body elided
}
현재 Rust는 호출 위치에서 매개변수를 언제나 왼쪽에서 오른쪽 순서로 평가합니다. 따라서 명명된 버전을 사용한다면 다음과 같습니다:
fn consume(data: Vec<u8>, length: usize) {
// body elided
}
fn consume2(length: usize, data: Vec<u8>) {
// body elided
}
fn main() {
let data = vec![1, 2, 3];
// this is an error, borrow after move
consume(data, data.len());
// this is okay
consume2(data.len(), data);
// does this compile or not?
consume(length: data.len(), data: data);
}
정의에 따른 왼쪽에서 오른쪽 평가 순서는 유지하되, 명명된 값을 어떤 순서로든 사용할 수 있게 한다면, 갑자기 호출 위치의 일부 순서는 컴파일되고 일부 순서는 컴파일되지 않습니다. 컴파일되는 쪽을 택하도록 평가 순서를 바꿔야 할까요? 그것은 매우 위험하고 혼란스럽게 느껴집니다.
Rust에서는 구조체 필드로는 이를 할 수 있습니다:
struct Args {
data: Vec<u8>,
length: usize,
}
fn main() {
let data = vec![1, 2, 3];
// works
let args = Args {
length: data.len(),
data,
};
// doesn't
let args = Args {
data,
length: data.len(),
};
}
이것이 명명된 인자를 위한 많은 제안이 구조체와 이를 통합하려는 이유 중 하나입니다. 앞서 말했듯이 극복할 수 없는 문제는 아니지만, 정리해야 할 사항입니다.
또 다른 작은 문제는 호환성 위험입니다. 매개변수에 이름을 붙이기 시작하면, 이름을 바꾸는 것이 호출자를 깨뜨립니다. 괜찮을까요? 이를 돕기 위해 별칭을 도입할 수 있을까요? 복잡성이 더 늘어납니다…
이것들은 설계 공간에서 나타나는 문제 중 일부일 뿐이며, 이 글이 길어지고 있으므로 지금은 여기까지만 하겠습니다. 하지만 이를 수행하려는 제안은 이 모든 것과 그 이상을 다뤄야 하며, 이것이 언어 설계가 어려운 이유입니다.
세상이나 세상에 대한 제 이해가 바뀌면서 저 역시 시간이 지나면 의견을 바꾼다고 생각하고 싶습니다. 그래서 10년 넘게 유지해 온 의견이 최근 조금 바뀐 방식을 공유하는 것이 흥미로울 수 있다고 생각했습니다. 명명된 매개변수가 Rust에 적합한지는 여전히 확신하지 못하지만, 요즘은 적어도 어느 정도 그 생각에 열려 있습니다. 누군가 계속 이를 추진하기로 한다면 언어 팀이 어떤 결정을 내릴지 보겠습니다. 실제로 그렇게 하는 사람들이 있습니다. 예를 들어 오늘 이 글을 보았기 때문에 이 글을 쓰는 것이기도 합니다. 이 글은 그것을 지지하거나 반박하려는 의도가 아닙니다. 사실 아직 읽어보지도 않았습니다. 그저 최근 Rue 작업을 하면서 이 문제를 많이 생각하고 있었고, 그 글을 본 일이 제 생각 일부를 적어 보게 했습니다. 그 글은 이 분야의 실제 제안이고, 이 글은 일반적인 아이디어와 이에 대한 제 생각을 탐구하는 쪽에 더 가깝기 때문에 아마 읽어 보는 편이 좋을 것입니다.
다음은 BlueSky에 올린 이 글에 관한 제 글입니다: