원본 링크: The worst programming language of all time
C++는 40년이 넘는 역사를 가진 언어이지만, 여전히 많은 개발자들에게 가장 다루기 어려운 언어 중 하나로 꼽힙니다. 압도적인 복잡성, 파편화된 생태계, 비직관적인 설계 결정들이 누적되면서 생산성 저하와 개발자 경험 악화를 초래하고 있습니다. 이 글에서는 C++가 왜 “최악의 프로그래밍 언어”라는 불명예를 얻게 되었는지, 그 구조적 문제점들을 살펴봅니다.
1. 기본조차 복잡한 언어 설계
C++는 변수 초기화 방식만 약 20가지에 달하며, 초기화 관련 규칙을 다룬 300페이지짜리 교과서가 존재할 정도로 복잡합니다. Python에서 print()로 간단히 출력할 수 있는 것과 달리, C++는 꺽쇠 괄호(<<)를 사용한 스트림 연산자를 써야 했습니다.
2023년에 표준 print 함수가 추가되었지만, 대부분의 산업 현장에서는 여전히 구형 C++ 버전을 사용하고 있어 최신 기능을 실무에 적용하기 어렵습니다. 난수 생성 같은 간단한 작업도 Python의 random.randint 호출과 달리, C++에서는 random_device, mt19937, uniform_int_distribution 등을 생성하고 연결하는 복잡한 과정을 거쳐야 합니다.
벤치마크 코드 작성 같은 간단한 작업도 JavaScript에 비해 훨씬 복잡하고 장황합니다. auto 키워드나 using 선언으로 코드 길이를 줄일 수 있지만, 멤버 변수에는 auto를 사용할 수 없고, 헤더 파일에 using을 사용하면 전역 네임스페이스를 오염시킬 수 있어 제약이 많습니다.
2. 캐스팅과 키워드의 모호성
C++의 형 변환은 다른 언어에 비해 매우 번거롭습니다. Java에서는 괄호 안에 타입을 넣어 간단히 캐스팅할 수 있지만, C++에서는 static_cast, dynamic_cast, reinterpret_cast, const_cast, bit_cast 등 컨텍스트에 따라 다른 키워드를 사용해야 합니다.
static 키워드는 함수 호출 간 변수 지속성, 클래스 변수 공유, 클래스 외부 접근 차단 등 3~4가지의 매우 다른 용도로 사용됩니다. private나 internal과 같은 명확한 키워드 대신 기존 키워드를 재활용하여 혼란을 가중시킵니다.
inline 키워드는 과거에는 컴파일러 최적화를 지시했으나, 현대 컴파일러가 이를 자동으로 처리하면서 의미가 퇴색되었습니다. 현재는 단일 정의 규칙(One Definition Rule) 관련 문제를 해결하는 데 주로 사용되지만, 함수와 변수에서의 의미가 상충되어 일관성이 부족합니다.
상속 시스템도 직관적이지 않습니다. Java의 interface 키워드와 달리, C++에서는 모든 함수에 virtual 키워드를 사용하고 함수를 = 0으로 초기화하는 독특한 방식으로 인터페이스를 만들어야 합니다.
3. 타입 시스템과 const의 혼란
C++는 약 50가지에 달하는 정수 타입을 제공하며, 이들 타입의 크기는 컴파일러와 대상 하드웨어에 따라 달라집니다. Windows 64비트에서 long이 32비트인 반면 Linux 64비트에서는 64비트인 경우가 있어 호환성 문제를 야기합니다.
char, unsigned char, wchar_t 등 7가지가 넘는 문자 타입의 존재와 이들의 차이점, std::string과 std::wstring의 차이점은 초보자에게 엄청난 혼란을 초래합니다.
const 키워드는 변수의 값이 상수임을 나타내지만, mutable const와 같은 역설적인 코드가 유효합니다. 멤버 변수를 const로 선언하면 객체를 다른 객체에 할당할 수 없게 되는 부작용도 있습니다. 포인터와 const의 조합(const int*와 int* const)은 더욱 복잡하며, 오른쪽에서 왼쪽으로 읽는 비직관적인 방법을 사용해야 합니다.
4. 헤더 파일의 악몽
헤더 파일은 C++에서 가장 비판받는 요소입니다. .cpp 파일마다 해당하는 헤더 파일을 생성하고 이들을 항상 동기화해야 하는 것은 DRY(Don’t Repeat Yourself) 원칙을 명백히 위배합니다.
함수 매개변수가 변경되면 .cpp 파일과 헤더 파일 모두를 수정해야 하며, 헤더 파일에 잘못된 매개변수 이름을 사용해도 컴파일러가 오류를 알려주지 않습니다. 클래스 생성자를 선언하고 정의하는 과정에서 클래스 이름을 네 번이나 반복해서 작성해야 하는 등 불필요한 타이핑이 많습니다.
헤더 파일을 포함하면 컴파일러가 해당 파일의 내용을 단순히 복사-붙여넣기하는 방식으로 작동합니다. 이로 인해 전이적 의존성 때문에 동일한 내용이 여러 번 포함될 수 있으며, 이는 단일 정의 규칙을 위반하여 컴파일 오류를 발생시킵니다.
이를 해결하기 위해 개발자들은 모든 헤더 파일에 헤더 가드를 추가해야 하는데, #ifndef/#define/#endif를 사용하는 전통적인 방식과 pragma once를 사용하는 방식 중 어떤 것을 선택할지에 대한 끊임없는 논쟁이 있습니다.
5. 네임스페이스의 한계
헤더 파일 시스템의 결함은 네임스페이스의 도입으로 이어졌습니다. C에서 이름 충돌 문제를 해결하기 위해 함수 이름에 프로젝트 접두사를 붙였던 것과 달리, C++에서는 네임스페이스를 사용합니다.
최신 언어들은 헤더 파일을 사용하지 않으므로 이름 충돌이 훨씬 적게 발생하며, 필요할 때 모듈 시스템을 통해 명확하게 처리합니다. C++에서 using 키워드를 사용하여 네임스페이스 접두사를 생략할 수 있지만, 이는 결국 이름 충돌 문제로 돌아가게 만듭니다.
개발자들은 짧고 모호한 네임스페이스 이름을 선호하게 되고, 이는 C++ 코드베이스를 읽기 어렵고 이해하기 어려운 약어로 가득 차게 만듭니다.
네임스페이스의 심볼 탐색 방식은 누군가 기존 네임스페이스 계층 구조와 일치하는 새 네임스페이스를 추가하면, 컴파일 시 어떤 심볼이 발견될지 예기치 않게 변경될 수 있습니다. 이는 다른 사람의 코드에 직접적인 변경 없이도 내 코드가 깨질 수 있는 사일런트 버그를 유발할 수 있습니다.
6. 긴 컴파일 시간
C++의 컴파일 시간은 헤더 파일의 복사-붙여넣기 방식 때문에 매우 길어집니다. .cpp 파일에서 포함하는 모든 내용이 매번 처음부터 컴파일되므로, 동일한 헤더 파일이 수백 개의 .cpp 파일에 포함되면 수백 번 컴파일되는 비효율이 발생합니다.
미리 컴파일된 헤더(Precompiled Headers, PCH)와 같은 해킹적인 방법이 있지만, 이는 언어의 근본적인 문제를 해결하는 것이 아니라 임시방편적인 해결책에 불과합니다.
C++ 20에서 도입된 모듈 기능은 헤더 파일 문제를 해결하도록 설계되었지만, 언어의 복잡성 때문에 컴파일러 개발자들이 구현에 어려움을 겪고 있습니다. 모듈이 널리 채택되기까지는 최소 10~15년이 더 걸릴 것으로 예상되며, 그동안 개발자들은 헤더 파일과 모듈을 혼용하는 과도기적 상황에 놓일 것입니다.
7. Modern C++의 모호함
초보 개발자들은 “모던 C++”를 배워야 한다는 조언을 자주 듣지만, 무엇이 “모던 C++”인지 명확하지 않습니다. 온라인 튜토리얼이나 교과서도 오래된 관행을 가르치거나, 중요한 개념을 깊이 있게 설명하지 못합니다.
스마트 포인터 중 shared_ptr가 실무에서는 코드 스멜로 간주되어 unique_ptr를 거의 항상 사용해야 한다는 사실을 튜토리얼에서는 제대로 알려주지 않습니다. 대학교에서도 new와 delete 키워드를 사용하도록 가르치는 등 시대에 뒤떨어진 내용을 교육합니다.
현재(2025년 기준) 산업계는 C++ 17에 머물러 있으며, 최신 표준인 C++ 23에 비해 6년 정도 뒤처져 있습니다. 개발자는 고용 가능성을 높이기 위해 구버전과 신버전 C++를 모두 학습해야 하는 현실에 직면합니다.
8. 빌드 시스템의 악몽
C++ 프로젝트를 빌드하는 것은 고통스러운 경험입니다. 표준 컴파일러, 표준 빌드 시스템, 표준 패키지 관리자, 표준 ABI가 없기 때문에 생태계는 심각하게 분열되어 있습니다.
주요 컴파일러는 Microsoft Visual C++, GCC, Clang 세 가지가 있으며, 각각 다른 최적화 기능, 확장 및 구현 속도를 가집니다. 컴파일러마다 새로운 C++ 기능 구현 속도가 다르고, 특정 기능에 대한 최적화 수준도 달라 개발자들은 끊임없이 논쟁합니다.
수백 가지에 달하는 컴파일러 플래그는 또 다른 복잡성을 더합니다. 플래그는 암호 같고, 컴파일러마다 동일한 기능에 다른 플래그를 사용하며, 최적화 수준에 따라 프로그램 동작이 미묘하게 달라져 디버깅하기 어려운 버그를 유발할 수 있습니다.
C++에는 표준 빌드 시스템이 없으므로, MSBuild, Xcode Build System, Make 등 다양한 빌드 도구가 난립하고 있습니다. 플랫폼 간 호환성을 위해 CMake, Premake, QMake, Meson과 같은 메타 빌드 시스템이 필요합니다.
CMake는 현대 오픈소스 프로젝트의 사실상 표준이 되었지만, 개발자들에게는 “끔찍한 빌드 시스템”으로 악명이 높습니다. 선언적이지도 절차적이지도 않은 자체 스크립트 언어를 사용하며, 특이한 스코핑 규칙, 변수 처리, 일관성 없는 API, 불량한 오류 메시지 등 수많은 문제점을 가지고 있습니다.
9. 의존성 관리의 지옥
다른 언어에서는 라이브러리 설치가 간단한 터미널 명령 하나로 이루어지지만, C++에서는 타사 의존성을 설치하고 관리하는 것이 악몽 같은 경험입니다.
C++에는 표준 ABI가 없어 컴파일러, 컴파일러 버전, 아키텍처, 컴파일러 플래그 등 모든 설정이 일치해야 합니다. C++ ABI는 컴파일러마다, 심지어 동일 컴파일러의 버전마다 다를 수 있기 때문에 C++는 바이너리 배포에 적합하지 않은 언어로 비판받습니다.
사전 컴파일된 C++ 라이브러리를 통합하는 과정은 더욱 복잡합니다. 정적 또는 동적 링크 방식을 결정해야 하고, 각각 다른 파일 형식을 사용하며 플랫폼마다 다릅니다. 라이브러리는 디버그/릴리즈 빌드, 헤더 파일 등으로 구성된 여러 파일 묶음이므로, 적절한 폴더 구조를 설정하고 IDE나 빌드 시스템에 링크하도록 설정해야 합니다.
라이브러리가 다른 라이브러리에 의존하고, 그 라이브러리들이 또 다른 라이브러리에 의존하는 의존성 지옥에 빠지기 쉽습니다. 헤더 파일의 매크로 때문에 #include 순서가 중요해지는 경우도 있는데, windows.h를 winsock2.h보다 먼저 포함하면 이해할 수 없는 오류가 발생하는 것이 대표적인 예입니다.
이러한 고통을 줄이기 위해 헤더 온리 라이브러리라는 관행이 생겨났지만, 이는 파일 크기가 수만~수십만 줄로 비대해지고 컴파일 시간이 크게 늘어나는 부작용을 낳습니다.
10. 표준 라이브러리의 불완전성
C++ 표준 라이브러리는 현대 디지털 시대에 필수적인 기본 기능(네트워킹, HTTP 요청, JSON 지원, 최신 IO, 유니코드 지원, 명령줄 인수 파싱 등)이 부족합니다. Go 언어의 표준 라이브러리가 이러한 측면에서 C++보다 훨씬 우수하다는 평가를 받습니다.
std::string은 멀티바이트 문자 인식이 부족하여 실제 문자열이 아닌 바이트 문자열에 가까우며, 유니코드 지원이 내장되어 있지 않습니다. 문자열 분할, 결합, 다듬기 등 기본적인 기능도 부족했고, starts_with, ends_with, contains와 같은 메소드는 2020년과 2023년에야 추가되었습니다.
std::vector 또한 기대하는 많은 메소드가 부족하고, 대신 표준 라이브러리의 자유 함수를 사용해야 합니다. 배열의 항목을 변환하는 JavaScript의 map 메소드와 달리, C++에서는 std::transform 자유 함수와 반복자, 람다식을 조합하는 장황하고 가독성이 떨어지는 방식을 사용해야 합니다.
표준 라이브러리 알고리즘의 많은 부분이 매개변수로 begin()과 end()를 요구하는데, 대부분의 경우 전체 컨테이너에 적용하고 싶어도 유연성을 명분으로 이들을 강제하여 코드를 길고 지저분하게 만듭니다.
std::vector<bool>은 실제 bool 타입의 벡터가 아니라 공간 절약을 위해 각 bool을 비트로 저장하는 특수한 구현으로, 일반적인 컨테이너 인터페이스를 제공하지 않고 성능 저하를 일으키는 설계 오류로 지적됩니다.
11. 오류 메시지의 난해함
C++의 오류 메시지는 엄청나게 복잡하고 난해합니다. 컴파일러는 단 하나의 문자만 잘못 입력해도 수천 줄의 이해할 수 없는 오류 메시지를 쏟아냅니다. 꺽쇠 괄호, 세미콜론, 밑줄, 표준 라이브러리의 구현 세부 사항이 뒤섞인 이 메시지들은 초보 개발자에게 엄청난 공포를 안겨줍니다.
템플릿은 오류 메시지를 수백만 배 더 악화시킵니다. 템플릿은 C++에서 제네릭 프로그래밍을 가능하게 하는 강력한 기능이지만, 800페이지짜리 교과서가 있을 정도로 매우 복잡합니다. 템플릿 관련 오류 메시지는 엄청나게 길고 암호 같으며, 4K 모니터에서도 한 화면에 다 들어오지 않을 정도입니다.
템플릿 메타 프로그래밍은 컴파일 시간 계산을 수행하는 강력하지만 가장 어려운 C++ 기능 중 하나입니다. 학습 곡선이 매우 가파르고 문법이 장황하며 난해하여 숙련된 개발자도 유지보수하기 어렵습니다.
특정 타입만 템플릿과 함께 사용되도록 제한하는 내장 지시어가 없어, SFINAE(Substitution Failure Is Not An Error)라는 기법이 고안되었지만, 그 이름조차 의미를 알기 어렵고 매우 장황하며 복잡한 템플릿 메타 프로그래밍 지식을 요구합니다.
12. 메모리 안전성의 부재
C++는 기본적으로 메모리 안전하지 않습니다. 매달린 포인터, 메모리 누수, 해제 후 사용, 이중 해제, 이동 후 사용, 버퍼 오버플로, 초기화되지 않은 메모리 접근, 널 포인터 역참조, 반복자 무효화, 수명 문제, 포인터 산술 오류와 같은 수많은 메모리 관련 문제가 발생할 수 있습니다.
이러한 문제들은 디버깅하기 매우 어렵고, 재현하기 힘들며, 때로는 디버거 자체의 정보를 손상시키기도 합니다. 데이터 경합이나 교착 상태와 같은 쓰레드 관련 문제는 더욱 복잡합니다.
미정의 동작(Undefined Behavior, UB)은 C++에서 가장 악명 높은 개념 중 하나입니다. 컴파일러가 임의의 최적화를 수행할 수 있는 권한을 가지며, 프로그램이 예측 불가능하게 동작하거나 보안 취약점으로 이어질 수 있습니다.
C++는 의료 기기와 같은 중요한 시스템에서 사용될 경우, 한 줄의 잘못된 코드도 사람의 생명을 위협할 수 있는 결함이 많고 취약하며 메모리 안전하지 않은 언어입니다.
13. Rust: 더 나은 대안
Rust는 C++의 많은 문제점을 해결하도록 설계된 언어입니다. 기본적으로 메모리 안전을 보장하며, 소유권 시스템을 통해 컴파일 타임에 대부분의 메모리 관련 버그를 잡아냅니다.
Rust는 현대적인 빌드 시스템(Cargo), 통합된 패키지 관리자, 명확한 오류 메시지, 헤더 파일이 없는 모듈 시스템을 제공합니다. 표준 라이브러리는 더 완전하고 사용하기 쉬우며, 언어 설계가 일관성 있고 직관적입니다.
Rust의 이동 시맨틱스는 단 한 페이지의 문서로 설명되는 반면, C++의 이동 시맨틱스는 300페이지짜리 교과서를 필요로 합니다. Rust는 기본적으로 변수가 불변이며, 가변성은 명시적으로 선택해야 합니다.
마치며
C++는 고성능이 요구되는 시스템 프로그래밍, 게임 엔진, 임베디드 시스템 등에서 여전히 중요한 역할을 하고 있습니다. 하지만 40년 이상 누적된 기술 부채, 하위 호환성 유지를 위한 타협, 파편화된 생태계는 개발자 경험을 심각하게 저해하고 있습니다.
새로운 프로젝트를 시작한다면, C++가 정말 필요한지 신중히 고려해야 합니다. Rust와 같은 현대적인 대안들이 존재하며, 많은 경우 더 나은 선택이 될 수 있습니다. 기존 C++ 코드베이스를 유지보수해야 한다면, Modern C++의 모범 사례를 따르고, 도구와 라이브러리를 현명하게 선택하는 것이 중요합니다.
블로그 주인장의 의견
자칭 C++ 애호가라고 친구들에게 장난삼아 이야기를 하지만, 저 내용에 대해 대부분 반박하기 힘들 정도로 사용성이 불편한 건 사실입니다. 헤더 파일 관리, 긴 컴파일 시간, 의존성 관리의 복잡함은 실무에서 정말 큰 고통입니다.
그럼에도 불구하고 C++를 계속 사용하는 이유는 성능이 중요한 영역에서는 여전히 대체할 만한 선택지가 많지 않기 때문입니다. Rust가 좋은 대안으로 떠오르고 있지만, 기존 C++ 코드베이스와의 통합, 생태계의 성숙도, 팀의 학습 곡선 등을 고려하면 전환이 쉽지 않습니다.
Quick questions
C++를 배우는 것이 여전히 가치가 있나요?
시스템 프로그래밍, 게임 개발, 고성능 컴퓨팅 분야에서 일하고 싶다면 여전히 가치가 있습니다. 하지만 새로운 프로젝트라면 Rust와 같은 현대적인 대안을 먼저 고려해보는 것이 좋습니다.
Modern C++를 배우면 이러한 문제들이 해결되나요?
Modern C++(C++11 이후)는 스마트 포인터, 람다, auto 키워드 등으로 일부 문제를 개선했지만, 헤더 파일, 빌드 시스템, 의존성 관리와 같은 근본적인 문제는 여전히 남아 있습니다.
Rust가 C++를 완전히 대체할 수 있나요?
새로운 프로젝트에서는 Rust가 많은 경우 더 나은 선택입니다. 하지만 기존 C++ 코드베이스, 레거시 시스템과의 통합, 팀의 기존 전문성 등을 고려하면 C++는 당분간 계속 사용될 것입니다.
이 포스트는 블로그 주인장이 흥미롭다고 생각하는 주제를 AI를 통해 요약한 글입니다.
주인장이 개인적으로 읽으려고 만든게 맞으니 참고 바랍니다!
