러스트에서 지나친 추상화, 제네릭, 조기 최적화가 부르는 복잡성을 경계하고, 읽기 쉽고 유지보수 가능한 단순함을 선택하자는 글. 단순함은 신뢰성과 성능의 기반이며, 팀과 사용자 경험을 개선한다.
새벽 3시에 휴대전화가 진동한다.
침대에서 몸을 일으켜 노트북을 열고, 로그에서 이것을 본다:
thread 'tokio-runtime-worker' panicked at 'called `Result::unwrap()` on an `Err` value:
Error("data did not match any variant of untagged enum Customer at line 1 column 15")',
src/parsers/universal.rs:47:23
코드베이스를 열어 이런 코드를 찾는다:
pub struct UniversalParser<T: DeserializeOwned> {
format: Box<dyn DataFormat>,
_marker: std::marker::PhantomData<T>,
}
impl<T: DeserializeOwned> UniversalParser<T> {
pub fn parse(&self, content: &str) -> Result<Vec<T>, Box<dyn std::error::Error>> {
self.format.parse(content)
}
}
몇 가지 생각이 스친다:
“PhantomData가 도대체 뭐야?”
“왜 트레이트 객체가 있지?”
“오늘 밤은 길겠군.”
에러는 분명 DataFormat
트레이트, 제네릭 파서, 그리고 serde 역직렬화의 상호작용 어딘가에 묻혀 있을 것이다. 200줄에 달하는 트레이트 구현과 제네릭 제약을 스크롤한다. 각 계층은 또 하나의 간접화를 추가한다. 스택 트레이스는 15단계나 깊다. 양파를 까는 것 같다… 눈물이 난다.
git blame
을 돌리고 이 코드를 쓴 동료를 욕한다. 헉, 몇 달 전의 나였다.
잠깐 되감기. 새벽 3시에 휴대전화가 진동한다.
침대에서 몸을 일으켜 노트북을 열고, 로그에서 이것을 본다:
Error: CSV parse error at line 847: invalid UTF-8 sequence at byte index 23
이 코드를 찾는다:
#[derive(Debug, Deserialize)]
pub struct Customer {
pub name: String,
pub email: String,
pub phone: String,
}
pub fn parse_customers(csv_content: &str) -> Result<Vec<Customer>, csv::Error> {
let mut reader = csv::Reader::from_reader(csv_content.as_bytes());
reader.deserialize().collect()
}
좋아, CSV 파일에서 고객 데이터를 파싱하고 있는 듯하다.
입력 파일의 847번째 줄을 보니 문자 인코딩이 깨져 있다. 문제의 줄을 지우고, 수정해 배포하고, 다시 잠자리에 든다.
러스트 프로그래머들은 대체로 영리하다. 때로는 지나치게. 나부터도 이런 죄를 많이 지었다고 솔직히 인정한다.
우리는 러스트를 한계까지 밀어붙이길 좋아한다. 어쨌든 이건 러스트 아닌가! 무한한 가능성을 주는 힘 있는 놀이터. 언어의 모든 기능을 최대한 활용해야 하지 않을까?
러스트는 우리에게 요란하게 굴도록 강요하지 않는다. 다른 언어들처럼 러스트에서도 직관적인 코드를 얼마든지 쓸 수 있다. 하지만 코드 리뷰에서 사람들은 종종 스스로를 능가하려다 자기 신발끈에 걸려 넘어진다. 유지보수성에 대해 깊이 고민하지 않은 채, 손에 잡히는 고급 기능을 전부 동원한다.
문제는 이거다: 코드 쓰기는 쉽지만 읽기는 어렵다. 이런 고급 기능은 소금과 같다. 약간은 풍미를 살리지만 너무 많이 넣으면 요리를 망친다. 그리고 고급 기능은 종종 사태를 과도하게 복잡하게 만들어 가독성을 떨어뜨린다.
소프트웨어 공학은 복잡성을 다루는 일이다. 복잡성은 우리가 못 본 사이에 스며든다. 우리는 복잡성을 낮추는 데 집중해야 한다.
물론 피할 수 없는 복잡성도 있다. 그것은 과제 자체의 본질적 복잡성이다. 그러나 피해야 하는 것은 우리가 스스로 도입하는 우발적 복잡성이다. 프로젝트가 커질수록 우발적 복잡성도 함께 자라난다. 바로 그 군더더기를 우리는 끊임없이 경계해야 한다.
단순함에는 다른 이점도 있다:
단순함은 신뢰성의 전제 조건이다.
나는 항상 Edsger W. Dijkstra의 말에 동의하지는 않지만, 이번만큼은 완전히 공감한다. 단순함 없이는 신뢰성이 불가능(적어도 달성하기 매우 어려움)하다. 단순한 시스템은 논리적으로 따져야 할 움직이는 부품이 적기 때문이다.
좋은 코드는 대체로 지루하다. 특히 프로덕션에서는. 단순한 것은 자명하다. 단순한 것은 예측 가능하다. 예측 가능함은 좋은 것이다.
그런데 단순함이 그렇게 “더 좋다”면 왜 그게 규범이 되지 못할까? 단순함을 성취하기가 어렵기 때문이다! 자연스럽게 나오지 않는다. 단순함은 보통 _첫 시도_가 아니라 _마지막 손질_에서 나온다. 1
단순함과 우아함은 달성하기 위해 많은 노력과 규율을 요구하기 때문에 인기가 없다.
잘 말했다, Edsger.
단순한 시스템을 만들려면 _노력_이 필요하다. 그리고 그것을 _단순하게 유지_하려면 더 많은 노력이 든다. 왜냐하면 우리는 끊임없이 엔트로피와 싸워야 하기 때문이다. 단순함에서 복잡함으로 가는 길은 그 반대보다 훨씬 쉽다.
다시 새벽 3시의 전화를 떠올려 보자.
첫 번째 버전의 코드는 시스템을 “유연하고 확장 가능하게” 만들고 싶었던 엔지니어가 만들었다. 두 번째 버전은 당장의 문제를 해결하려고 CSV 파일을 파싱한 개발자가 썼다.
결국 CSV 외의 다른 것을 파싱해야 할 필요는 한 번도 없었다. 여기서 얻는 교훈 하나: 복잡성으로 가는 길은 선의로 포장되어 있다. 각각은 전적으로 합리적인 결정들의 연쇄가 지나치게 복잡하고 유지보수하기 어려운 시스템으로 이어질 수 있다. 개별적으로 보면 각 작은 복잡성은 무해해 보인다. 하지만 복잡성은 금방 눈덩이처럼 불어난다.
경험 많은 개발자일수록 더 많은 추상화를 쓰는 경향이 있다. 가능성에 흥분하기 때문이다. 그 마음을 탓할 수는 없다. 단순한 코드를 쓰는 일은 대체로 꽤 지루하다. 방금 배운 새 기능을 시험해 보는 게 훨씬 재미있다. 하지만 시간이 지나면 러스트 초보자가 우리 코드를 보며 어떤 기분일지 잊게 된다. 이것이 바로 지식의 저주다.
기억하자: 추상화는 결코 무비용이 아니다.2
모든 추상화가 똑같이 만들어지는 것은 아니다.
사실, 많은 것들은 추상화조차 아니다 — 실제 가치를 더하지 않으면서 복잡성만 더하는 얇은 베니어, 즉 간접화의 층일 뿐이다.
추상화는 복잡성을 낳고, 복잡성은 아주 현실적인 비용을 가진다. 어느 시점이 되면 복잡성은 인지 부하를 높여 당신을 느리게 만든다. 그리고 인지 부하는 정말 중요하다.
러스트를 시작하는 사람들은 종종 언어의 복잡성에 압도된다. 당신이 러스트에 익숙해질수록 이를 염두에 두라. 그렇지 않으면 경험이 적은 팀원을 소외시켜 프로젝트나 러스트 자체를 포기하게 만들 수도 있다.
게다가 당신이 회사를 떠나 복잡한 코드베이스를 남기면, 팀은 유지보수와 신규 온보딩에 큰 어려움을 겪을 것이다. 가장 큰 병목은 사람들이 얼마나 빨리 러스트에 익숙해질 수 있는가다. 그들을 더 어렵게 만들지 말자. 때때로 초보자의 눈으로 러스트를 바라보라.
왠지 제네릭에 대해 잠깐은 꼭 하고 싶은 말이 있다…
제네릭은 코드를 이해하기 어렵게 만들 뿐 아니라 컴파일 시간에도 실제 비용을 유발한다. 각 제네릭은 모노모픽화(단형화)된다. 즉, 컴파일 시 그 제네릭과 함께 사용된 각 타입마다 별도의 코드 사본이 생성된다.
내 조언은 이렇다. 지금 당장 구현을 바꿔 끼워야 할 필요가 있을 때만 제네릭으로 만들어라. 섣부른 일반화를 참아라! (이는 섣부른 최적화와 관련 있지만 동일하지는 않다.)
“나중에 필요할지도 몰라”는 위험한 말이다. 미래를 예측하기 어렵기 때문에 그 가정에 주의하라. 3
당신의 아름다운 추상화가 가장 큰 적이 될 수도 있다. 결정을 조금이라도 미룰 수 있다면, 그게 종종 더 낫다.
제네릭은 코드베이스 전체의 “감각”에 영향을 준다. 제네릭을 많이 쓰면, 그 여파를 모든 곳에서 감당해야 한다. 함수와 구조체의 시그니처, 그리고 그에 따른 에러 메시지를 이해해야 한다. 제네릭의 숨겨진 컴파일 비용은 측정하기도, 최적화하기도 어렵다.
제네릭을 신중히 다뤄라. 비용이 진짜 있다! 사고방식은 “이건 본질적으로 제네릭 기능이다”가 되어야지 “이걸 제네릭으로 만들 수도 있겠다”가 되어서는 안 된다.
가령 공개 API를 작업 중이라고 하자. 사용자로부터 문자열 기반 데이터를 많이 받게 될 함수가 있다. 입력을 &str
로 받을지, String
으로 받을지, 아니면 다른 걸로 받을지 고민한다.
fn process_user_input(input: &str) {
do something with input
}
꽤 단순하고 할당도 없다. 하지만 호출자가 String을 넘기고 싶다면?
fn process_user_input(input: String) {
do something with input
}
입력의 소유권을 가져온다. 그런데 잠깐, 소유권이 필요 없고 둘 다 지원하고 싶다면?
fn process_user_input(input: impl AsRef<str>) {
do something with input
}
된다. 하지만 복잡도가 올라가는 게 보이는가?
무대 뒤에서는 AsRef<str>
를 구현하는 각 타입마다 함수가 모노모픽화된다.
즉, String
과 &str
을 넘기면 그 함수의 사본이 두 개 생긴다. 컴파일 시간이 길어지고 바이너리도 커진다.
잠깐, 입력을 참조하는 무언가를 반환해야 하고, 그 결과가 입력만큼 오래 살아야 한다면?
fn process_user_input<'a, S>(input: &'a S) -> &'a str
where
S: AsRef<str> + ?Sized,
{
do something with input
}
아, 이걸 스레드 간에 보내야 할 수도 있네:
fn process_user_input<'a, S>(input: &'a S) -> &'a str
where
S: AsRef<str> + Send + Sync + ?Sized,
{
do something with input
}
단순한 &str
매개변수에서 이 괴물 같은 시그니처까지 어떻게 왔는지 보이는가? 각 단계는 “합리적”으로 보였지만, 아무도 읽거나 디버그하고 싶지 않은 악몽을 만들었다.
문제는 매우 단순한데, 복잡성은 어떻게 스며들었나? 영리하려고 했기 때문이다! 더 제네릭하게 만들어 함수를 “더 나아지게” 하려 했다. 그런데 정말 “더 나아진” 걸까?
우리가 원했던 것은 문자열 하나를 받아서 뭔가 하는 단순한 함수였다.
단순하게 가라. 과도하게 고민하지 마라!
링크 체커(link checker)를 만든다고 해 보자. 링크를 확인하기 위한 요청들을 잔뜩 만들어야 한다. Vec<Result<Request>>
를 반환하는 함수를 쓸 수 있다.
fn create_requests(urls: Vec<String>) -> Vec<Result<Request>> {
urls.into_iter()
.map(|url| create_request(&url))
.collect()
}
혹은 이터레이터를 반환할 수도 있다:
fn create_requests(urls: Vec<String>) -> impl Iterator<Item = Result<Request>> {
urls.into_iter().map(|url| create_request(&url))
}
이터레이터도 나쁘지 않아 보이지만, 벡터가 더 단순하다. 어떻게 할까? 호출자는 어차피 결과를 수집해야 할 가능성이 크다. 유한한 URL 집합을 처리하고, 링크 체커는 성공/실패를 보고하려면 모든 결과가 필요하며, 결과는 아마 여러 번 순회될 것이다. 문서 내 URL 수는 보통 작기 때문에 메모리 사용량도 큰 문제가 아니다. 다른 조건이 같다면, 벡터가 아마 더 단순한 선택이다.
단순한 코드는 느리다는 편견이 있다. 사실은 그 반대인 경우가 많다! 효과적인 알고리즘 중 다수가 놀라울 정도로 단순하다. 실제로 우리가 발견한 가장 단순한 알고리즘들 중 몇몇은 가장 효율적이기도 하다.
예를 들어 퀵소트나 경로 추적(path tracing)을 보라. 둘 다 몇 줄로 적을 수 있고, 몇 문장으로 설명할 수 있다.
아래는 러스트로 적은 즉흥적인 퀵소트다:
pub fn quicksort(mut v: Vec<usize>) -> Vec<usize> {
let Some(pivot) = v.pop() else {
return v;
};
let (smaller, larger) = v.into_iter().partition(|x| x < &pivot);
quicksort(smaller)
.into_iter()
.chain(std::iter::once(pivot))
.chain(quicksort(larger))
.collect()
}
아이디어는 아주 단순하고 냅킨에 적을 수 있다:
구현은 알고리즘 설명과 크게 다르지 않다.
그래, 이 단순한 버전은 지금은 usize
만 지원하고, 실제 용도라면 내장 정렬 알고리즘을 써야 한다. 내 벤치마크에선 위 버전보다 20배 빠르다. 하지만 요지는, 단순한 코드도 강력하다는 것이다. 내 머신에선 이 구현이 10만 개 숫자를 1밀리초 만에 정렬한다.
이건 O(n log n) 알고리즘이다. 비교 기반 정렬에서는 이 정도가 한계고, 코드 몇 줄이면 된다. 4
대개 단순한 코드는 컴파일러가 최적화하기 쉬워 CPU에서 더 빨리 돈다. CPU는 기본 자료구조와 예측 가능한 접근 패턴에 맞게 최적화되어 있기 때문이다. 병렬화도 그런 경우 더 쉽다. 우리 코드가 단순할 때 이런 모든 것들이 우리에게 유리하게 작용한다.
약간 역설적이지만, 특히 복잡한 일을 할수록 더더욱 단순함을 유지하려고 조심해야 한다. 단순함은 깊은 통찰, 높은 이해, 명료함의 신호이며—이 명료함은 시스템의 작동 방식에 긍정적 영향을 준다. 그리고 복잡한 시스템은 그 자체로 복잡하기 때문에, 그 추가적인 명료함이 통제를 유지하는 데 도움을 준다.
내가 러스트를 좋아하는 이유는 고수준과 저수준 프로그래밍의 균형이다. 우리는 이를 잘 활용해야 한다. 대부분의 시간에는 러스트 코드를 직설적으로 쓰고, 성능이 정말로 중요해지는 순간이 오면 언제든 돌아가 최적화할 수 있다.
회사에서 쓰는 코드의 대부분은 라이브러리 코드가 아니라 애플리케이션 코드다. 대부분의 회사는 라이브러리로 돈을 버는 게 아니라 비즈니스 로직으로 돈을 벌기 때문이다. 여기서 멋부릴 필요 없다. 애플리케이션 코드는 직관적이어야 한다.
라이브러리 코드는 조금 다를 수 있다. 다른 코드의 중요한 빌딩 블록이 되면 복잡해질 수 있다. 예를 들어, 성능 핵심 경로에서는 할당을 피하는 것이 타당할 수 있고, 그때는 라이프타임을 다뤄야 할 수도 있다. 타인이 코드를 어떻게 쓸지 모른다는 불확실성은 과도한 추상화를 부른다. 흔한 사용 사례를 직관적으로 만들려고 하라. 올바른 경로가 사용자가 자연스럽게 택하는 경로여야 한다.
예를 들어 base64 인코더를 만든다고 하자. 대부분의 사람은 문자열(아마 &str
같은 유니코드 문자열)을 인코딩하고 싶어 할 것이고, “표준” base64 인코딩을 쓰고 싶어할 것이다. _가장 흔한 일_을 하려고 사용자에게 점프를 요구하지 마라. 정말로 좋은 이유가 없는 한, API에는 이런 함수가 어딘가에 있어야 한다:
/// 입력을 Base64 문자열로 인코딩
fn base64_encode(input: &str) -> String;
물론 AsRef<[u8]>
에 대해 제네릭하게 만들거나 여러 알파벳을 지원할 수도 있다:
/// 여러 알파벳을 지원하는 제네릭 base64 인코더
fn base64_encode<T: AsRef<[u8]>>(input: T, alphabet: Base64Alphabet) -> String;
…그리고 최대한의 유연성을 위해 빌더 패턴을 제공할 수도 있다:
let encoded = Base64Encoder::new()
.with_alphabet(Base64Alphabet::UrlSafe) // UrlSafe가 뭐지?
.with_decode_allow_trailing_bits(true) // 뭐라고?
.with_decode_padding_mode(engine::DecodePaddingMode::RequireNone) // 이건 또...
.encode("Hello, world!");
하지만 대부분의 사용자가 원하는 건 인코딩된 문자열 하나다:
let encoded = base64_encode("Hello, world!");
위 함수를 base64_encode_simple
이나 base64_encode_standard
처럼 이름 붙여 보다 일반적인 알고리즘의 단순화 버전임을 드러내도 된다. 추가 기능을 제공해도 좋다. 하지만 그 과정에서 쉬운 일을 어렵게 만들지는 마라.
단순함은 특히 다른 개발자들과 함께 일할 때 중요하다. 코드는 아이디어를 전달하는 수단이며, 당신의 생각을 명확하게 표현하려고 애써야 한다.
Jerry Seinfeld에게는 두 가지 글쓰기 모드가 있었다: 창작 모드와 편집 모드.
이 두 모드는 서로 다른 마음가짐을 요구한다. 동시에 하려 들면 마비가 온다. 그래서 Seinfeld는 창작 중에는 절대 편집하지 않았다. 창작의 흐름을 죽이기 때문이다.
코딩에도 같은 원칙이 적용된다. 첫 시도에서 완벽한 아키텍처를 설계하려 하지 마라. 먼저 순진한 구현을 쓰고, 그다음 내면의 편집자가 다듬게 하라. 내면의 비평가를 잠시 꺼둬라. 어쩌면 더 단순한 설계를 떠올릴지도 모른다.
손에 쥔 날카로운 도구들을 모두 쓰고 싶은 유혹이 든다. 하지만 정말 날카로운 도구들이다! 러스트를 잘 쓴다는 건 이런 도구들에 “예스”라고 말하는 것보다 “노”라고 말할 줄 아는 것이다.
최적화 기회가 보이면 바로 뛰어들고 싶을 수 있다. 하지만 나는 수없이 사전 검증 없이 최적화를 해버리는 모습을 봐왔다. 결과는 성능이 그대로이거나 오히려 더 느려지는 것이다. 두 번 재고, 한 번 잘라라.
직관에 반하는 말일 수 있다. 끊임없이 리팩터링하면 갈수록 코드가 좋아지는 거 아닌가?
문제는 첫 프로토타입을 쓸 때 정보가 제한적이라는 점이다. 너무 일찍 리팩터링하면 출발점보다 더 나쁜 곳에 도달할 수 있다.
처음의 CSV 내보내기 예를 다시 보자. 영리한 엔지니어가 여러 입력 포맷을 지원하도록 리팩터링할 기회를 봤다. 그 덕분에 우리는 제네릭 내보내기에 갇혔고, 디버깅 부담은 커졌으며, 리팩터링을 미뤘다면 더 나은 추상화를 볼 수 있었을 기회를 잃었다. 어쩌면 항상 CSV 데이터만 다루지만, 데이터 검증과 데이터 내보내기를 분리할 수 있음을 깨달았을지 모른다. 그랬다면 이런 식의 더 나은 에러 메시지가 나왔을 것이다:
Error: Customer 123 has invalid address field: invalid UTF-8 sequence at byte index 23: Address: "123 M\xE9n St."
우리는 성급히 리팩터링한 탓에 이 기회를 놓쳤다.
나는 우선 눈앞의 문제를 해결하고, 그다음 리팩터링할 것을 제안한다. 단순한 프로그램을 리팩터링하는 편이 복잡한 프로그램을 리팩터링하는 것보다 훨씬 쉽기 때문이다. 전자는 누구나 하지만, 후자는 손에 꼽는다. 코드에 리팩터링의 기회를 남겨 둬라. 그 순간엔 똑똑해 보일 수 있지만, 단순한 코드가 조금 더 오래 머물도록 허용하면, 올바른 리팩터링의 타이밍이 저절로 드러나기도 한다.
되돌아볼 만한 좋은 시점은 코드가 반복적으로 느껴지기 시작할 때다. 데이터에 숨겨진 패턴이 있다는 신호다. 옳은 추상화가 당신에게 말을 걸며 모습을 드러내려 한다! 추상화는 여러 번 시도해도 괜찮다. 무엇이 잘 맞는지 보라. 어느 것도 아니다 싶으면, 단순한 버전으로 돌아가고 시도를 문서화하라.
러스트는 정말 빠르다. 그러니 말하자면 퍼포먼스 “범죄”를 마음껏 저질러도 된다. 아낌없이 clone하고, 같은 자료구조를 여러 번 순회하고, 해시맵이 벅차면 벡터를 써라.
정말 상관없다. 하드웨어는 빠르고 싸다. 그러니 일을 시켜라.
위의 모든 말이 추상화를 배우지 말라는 뜻은 아니다. 배우는 건 재미있고, 아는 것은 힘이다.
하지만 자신을 해치지 않으면서도 새 개념을 배울 수 있다. 매크로, 라이프타임, 내부 가변성 등등을 이해하는 건 매우 유용하지만, 일상적인 “평범한” 러스트 코드에서는 이런 개념을 거의 쓰지 않는다. 너무 집착하지 마라.
필요한 기능은 모두 쓰되, 필요 없는 기능은 쓰지 마라.
내가 즐겨 쓰는 리트머스 시험지는 “새 기능을 추가할 때 기분이 좋은가?”이다.
좋은 추상화는 서로 “딱” 맞물린다. 추상화 사이에 겹침이 없고, 쓸데없는 허드렛일이나 추가 변환이 필요 없는 느낌이다. 다음 단계가 항상 자명하게 느껴진다. 과도한 mocking 없이 테스트가 잘 돌고, 구조체 문서는 저절로 써진다. 문서에 “이 구조체는 X와 Y를 한다”고 적혀 있지 않다. X 또는 Y다. 동료에게 설계를 설명하기도 쉽다. 이때가 바로 승기를 잡은 때다. 거기까지 가는 길은 쉽지 않다. 많은 반복이 필요하다. 인기 라이브러리에서 보이는 결과물은 종종 그런 과정의 산물이다.
올바른 추상화는 올바른 일을 하도록 길잡이가 된다. 새 기능을 어디에 추가해야 할지, 버그를 어디서 찾아야 할지, 데이터베이스 쿼리를 어디에 넣어야 할지, 자명한 위치를 알려준다.
코드가 단순하면 이 모든 것이 더 쉬워진다. 그래서 숙련된 개발자는 추상화를 만들 때 늘 단순함을 염두에 둔다.
영리한 엔지니어가 저지르는 가장 흔한 실수는, 애초에 존재해서는 안 될 것을 최적화하는 것일 수 있다. 다리를 건널 때가 오면 그때 건너라.
영리하려 들지 말고, 명확하라. 컴퓨터가 아니라 사람이 읽을 코드를 써라.
단순함은 명료함이다. 단순함은 사물의 본질을 간결하게 표현하는 것이다. 단순함은 불필요한 것, 무관한 것, 잡음을 제거하는 것이다. 단순함은 좋다. 단순하라.
글쓰기와도 비슷하다고 생각한다. 우아함은(내 생각엔 단순함과 상관이 깊다) 끊임없는 개선의 반복 과정을 요구한다. 대부분의 글을 훌륭하게 만드는 것은 편집 과정이다. 1657년, Blaise Pascal은 “이 편지를 길게 쓴 것은 그것을 짧게 만들 시간이 없었기 때문이다.”라고 썼다. 나는 글을 쓸 때 이 말을 자주 떠올린다. ↩
참고로, NASA/JPL Laboratory for Reliable Software의 Gerard J. Holzmann이 쓴 “The Power of 10 Rules”을 보라. ↩
내가 잘 안다. 미래를 정확히 예측할 능력이 없어 매우 위험하지만 수익성 높은 투자 기회를 몇 번이나 놓쳤기 때문이다. ↩
물론 이것이 퀵소트의 가장 효율적인 구현은 아니다. 중간 벡터를 많이 할당하고, 최악의 경우 O(n^2) 성능을 가진다. 부분적으로 정렬된 데이터에 대한 최적화, 더 나은 피벗 선택 전략, 제자리 분할 등도 있다. 하지만 그것들은 어디까지나 최적화일 뿐이다. 핵심 아이디어는 그대로다. ↩