운영 효율보다 물리적 성능과 제어가 우선인 환경에서는 C++가 여전히 주력 선택지다.
Why It Works
이럴 때 특히 힘을 발합니다
성능 예산을 직접 다룬다
메모리와 실행 비용을 정밀하게 통제해야 하는 환경에서 다른 언어보다 훨씬 세밀한 선택이 가능하다.
하드웨어와 가깝다
센서, 카메라, 드라이버, 장비 제어와 붙는 시스템에서 깊은 통합이 필요할 때 강력하다.
수명 긴 제품군에 많다
기계 설비, 검사 장비, 데스크톱 제품, 엔진 계열 소프트웨어처럼 장기 유지가 필요한 분야에 자산이 많다.
Business Scenes
사업 현장에서 자주 보이는 장면
비전 검사 장비
라인 속도를 따라가며 이미지 판정을 내려야 하는 제조 현장에서 성능 여유를 확보하기 좋다.
로보틱스 제어
센서 입력과 모터 제어가 밀리초 단위로 엮이는 환경에서 낮은 레벨 제어가 가능하다.
게임/시뮬레이션 엔진
렌더링과 물리 계산이 중요한 분야에서 오랫동안 축적된 자산을 활용할 수 있다.
Where It Wins
아직도 C++로 쓰는 것들
C++를 고르는 이유는 대체로 하나로 모입니다. 실행 시간과 메모리 사용을 예측 가능한 범위 안에 묶어 두어야 하고, 그 예측이 깨지면 제품이 곧바로 망가지기 때문입니다. 카메라가 초당 수십 장을 보내는 검사 장비에서 판정이 한 프레임 늦으면 불량품이 그대로 다음 공정으로 넘어갑니다. 모터 제어 루프가 주기를 놓치면 기구가 흔들립니다. 게임 엔진에서 프레임 하나를 놓치면 사용자가 바로 알아챕니다. 이런 곳에서는 평균 성능보다 최악의 경우가 중요하고, 런타임이 알아서 메모리를 정리해 주는 대신 정리 시점을 알 수 없는 구조가 부담이 됩니다.
또 하나의 이유는 자산입니다. 영상 처리, 수치 해석, CAD, 오디오, 시뮬레이션 분야에서 오래 다듬어진 라이브러리와 장비 제조사가 배포하는 SDK는 상당수가 C 또는 C++ 인터페이스로 나옵니다. 다른 언어에서 쓰려면 결국 바인딩을 거쳐야 하고, 바인딩 계층이 성능과 디버깅 난이도를 동시에 갉아먹는 경우가 많습니다. 하드웨어에 붙는 코드를 직접 쓸 계획이라면 처음부터 같은 언어로 가는 편이 단순합니다.
반대로 C++가 답이 아닌 경우도 분명합니다. 요구사항이 자주 바뀌는 사내 업무 시스템, 화면 위주의 웹 서비스, 데이터 분석 파이프라인, 짧은 주기로 실험을 반복하는 제품에서는 얻는 것보다 잃는 것이 큽니다. 빌드 시간이 길고, 의존성 추가가 한 줄로 끝나지 않고, 사소한 실수가 크래시나 조용한 메모리 오염으로 돌아옵니다. 성능이 문제가 아니라 변경 속도가 문제인 상황이라면 다른 언어를 쓰고, 정말 무거운 계산 부분만 C++로 떼어 내 붙이는 편이 낫습니다. 언어별 성격 차이는 언어 가이드 목록과 문법 비교에서 나란히 놓고 보면 판단이 빨라집니다.
객체 수명과 RAII가 사고방식을 바꿉니다
가비지 컬렉션이 있는 언어에서 넘어오면 가장 먼저 부딪히는 것이 객체 수명입니다. C++에서는 지역 변수가 스코프를 벗어나는 순간 소멸자가 실행됩니다. 언제 실행될지 모르는 것이 아니라, 그 중괄호를 닫는 지점에서 반드시 실행됩니다. 이 결정적 소멸이 C++ 설계의 중심입니다. 파일 핸들, 뮤텍스 잠금, 카메라 세션, 소켓처럼 반드시 풀어 줘야 하는 자원을 객체의 수명에 묶어 두면, 예외가 나든 중간에 return하든 정리 코드가 빠질 수 없습니다. 이 관용구를 RAII라고 부르고, 실무 C++ 코드의 상당 부분이 이 규칙 위에 서 있습니다. 다른 언어에서 try/finally나 defer로 매번 적어 주던 일을 타입이 대신 해 준다고 보면 됩니다.
두 번째로 부딪히는 것은 값 의미론입니다. C++에서 변수 대입과 함수 인자 전달은 기본이 복사입니다. 참조를 넘긴다고 생각하고 큰 std::vector<int>를 인자로 받으면, 호출할 때마다 통째로 복사됩니다. 그래서 읽기만 할 때는 const 참조로 받고, 소유권을 넘길 때는 std::move로 이동시킵니다. std::move는 무언가를 옮기는 함수가 아니라 "이 객체의 내용을 가져가도 된다"고 표시하는 캐스트에 가깝고, 실제 이동은 이동 생성자와 이동 대입 연산자가 합니다. 이동한 객체는 유효하지만 값은 정해지지 않은 상태로 남기 때문에, 옮긴 뒤에 다시 읽는 코드는 버그입니다.
세 번째는 포인터입니다. 요즘 코드에서 raw new와 delete를 직접 짝지어 쓰는 일은 드뭅니다. 소유자가 하나뿐이면 unique_ptr<T>, 여러 곳이 나눠 가져야 하면 shared_ptr<T>를 쓰고, 순환 참조가 생길 자리에는 weak_ptr를 끼웁니다. 여기서 중요한 것은 문법이 아니라 습관입니다. 새 타입을 만들 때마다 "이 객체는 누가 소유하고, 언제 죽는가"를 먼저 정하는 것. 소유권이 애매한 채로 굴러가는 코드는 언젠가 이중 해제나 댕글링 포인터로 돌아옵니다. 메모리 모델 자체를 정리하고 싶다면 메모리 기초 정리를 같이 보면 도움이 됩니다.
정의되지 않은 동작이라는 지뢰
C++에서 가장 낯선 개념은 정의되지 않은 동작입니다. 배열 범위를 벗어난 접근, 해제된 메모리 사용, 초기화하지 않은 값 읽기, 부호 있는 정수 오버플로 같은 것들은 "에러가 난다"가 아니라 "무슨 일이 벌어져도 규격상 문제없다"에 해당합니다. 즉시 크래시가 나면 차라리 운이 좋은 쪽입니다. 실제로는 몇 달 동안 멀쩡히 돌다가, 컴파일러를 올리거나 최적화 옵션을 바꾸거나 구조체에 필드를 하나 추가한 순간 갑자기 이상해집니다. 원인 코드와 증상 사이가 시간적으로도 공간적으로도 멀어서, 로그만 보고 좁히기가 어렵습니다.
그래서 C++ 팀은 "돌아가니까 맞다"를 근거로 삼지 않습니다. 대신 도구로 확인합니다. 주소 검사기와 정의되지 않은 동작 검사기를 켠 빌드로 테스트를 돌리고, 컴파일러 경고를 최대로 올린 뒤 경고를 에러로 취급합니다. 인덱스 계산과 버퍼 크기를 직접 다루는 코드는 리뷰에서 가장 오래 붙잡는 부분입니다. 실패를 값으로 다루는 방식은 C++ 에러 처리 가이드에서 더 자세히 다룹니다.
디버그 빌드에서만 테스트하고 릴리즈 빌드로 출하하는 흐름은 특히 위험합니다. 디버그 빌드는 스택을 넉넉히 잡고 변수를 채워 두는 경우가 많아, 초기화 누락이나 미세한 범위 초과가 가려집니다. 최적화가 켜진 릴리즈 빌드에서는 컴파일러가 "정의되지 않은 동작은 일어나지 않는다"를 전제로 코드를 재배치하기 때문에, 같은 소스가 전혀 다르게 동작합니다. 현장에서만 재현되는 버그는 대부분 여기서 나옵니다. 릴리즈와 동일한 최적화 옵션으로도 테스트를 한 번 더 돌리는 것을 기본 절차로 잡아 두는 편이 안전합니다.
Workflow
팀이 움직이는 방식
- 최적화는 코딩 전에 지연 예산을 숫자로 잡는 일부터 시작한다.
- 하드웨어 연동은 에러 처리와 복구 경로를 먼저 설계한다.
- 메모리 소유권이 애매한 코드는 기능보다 먼저 정리한다.
- 현장 장애는 기능 실패보다 타이밍 실패 관점에서 본다.
판정 결과를 구조로 명시하기
struct FrameDecision {
bool reject;
float confidence;
};
FrameDecision inspect(float score) {
return {score < 0.82f, score};
}
C++ 실무에서는 “빠르다”보다도 결과와 비용을 예측할 수 있게 만드는 구조가 중요하다.
Reading The Code
위 예제가 왜 저렇게 생겼는지
앞의 예제는 판정 결과를 bool 하나로 돌려주지 않고 구조체로 묶었습니다. 이유는 두 가지입니다. 첫째, 함수가 true/false만 돌려주면 호출한 쪽에서 "왜 그렇게 판정했는지"를 알 수 없어, 나중에 임계값을 조정하거나 오판정을 추적할 때 근거가 남지 않습니다. 점수를 함께 반환하면 로그와 재현 데이터가 같이 쌓입니다. 둘째, 반환 타입이 struct라도 비용 걱정을 할 필요가 거의 없습니다. 작은 값 타입이고, 컴파일러가 반환값을 호출자의 자리에 직접 만들어 주기 때문에 복사가 끼어들 자리가 없습니다. C++에서 "구조체로 묶으면 느려진다"는 직관은 대체로 맞지 않습니다. 느려지는 것은 힙 할당과 간접 참조이지 값 묶음이 아닙니다.
중괄호만 쓴 반환 return {score < 0.82f, score}; 도 의도가 있습니다. 필드 순서에 맞춰 그 자리에서 객체를 만들기 때문에 임시 객체를 만들었다가 대입하는 단계가 없습니다. 다만 필드 순서에 의존하므로, 같은 타입 필드가 여러 개인 구조체에서는 순서를 바꿨을 때 조용히 의미가 뒤집힙니다. 필드가 늘어나면 이름을 지정해 초기화하거나 생성자를 두는 편이 안전합니다. 임계값 0.82f를 코드에 그대로 박아 둔 것은 예제라서이고, 실제 장비 코드에서는 설정으로 빼서 라인별로 다르게 잡습니다.
자원을 객체 수명에 묶는 RAII
class CameraSession {
public:
explicit CameraSession(int device_id)
: handle_(camera_open(device_id)) {
if (!handle_) throw std::runtime_error("camera open failed");
}
~CameraSession() { camera_close(handle_); }
CameraSession(const CameraSession&) = delete;
CameraSession& operator=(const CameraSession&) = delete;
Frame grab() { return camera_grab(handle_); }
private:
CameraHandle* handle_;
};
void run_line(int device_id) {
CameraSession cam(device_id); // 여기서 열고
std::vector<FrameDecision> results;
results.reserve(1024);
for (int i = 0; i < 1024; ++i) {
results.push_back(inspect(score_of(cam.grab())));
}
report(std::move(results)); // 복사 대신 이동
} // 스코프를 벗어나며 닫힘
중간에 예외가 나든 일찍 return하든 camera_close는 반드시 한 번 실행됩니다. 복사 생성자를 지운 것은 핸들이 두 객체에 공유되어 두 번 닫히는 사고를 컴파일 단계에서 막기 위해서입니다.
이 예제에는 실무 습관이 몇 개 들어 있습니다. reserve로 벡터 용량을 미리 잡아 재할당을 없앤 것, 결과를 넘길 때 std::move로 이동시켜 큰 버퍼 복사를 피한 것, 그리고 소유권을 복사할 수 없게 막은 것입니다. 컨테이너 선택과 비용 감각은 C++ 컬렉션 가이드, 클래스 설계 쪽은 C++ 객체지향 가이드에서 이어서 볼 수 있습니다.
빌드 시스템이 업무의 일부입니다
다른 언어에서 오면 가장 당황스러운 점은 표준 패키지 매니저가 없다는 사실입니다. 라이브러리 하나를 추가하는 일이 한 줄 명령으로 끝나지 않고, 어디서 받아 어떻게 빌드하고 어떤 옵션으로 링크할지를 팀이 결정해야 합니다. 사실상의 공통 기반은 CMake이고, 여기에 vcpkg나 Conan 같은 패키지 매니저를 얹을지 말지가 프로젝트 초반의 선택지가 됩니다. 얹으면 의존성 관리가 편해지는 대신 그 도구의 규칙과 빌드 환경을 함께 짊어지고, 얹지 않으면 서드파티를 직접 소스로 넣거나 시스템 패키지에 의존하게 됩니다. 어느 쪽이든 누군가는 빌드를 계속 돌봐야 합니다.
컴파일러가 여러 개라는 점도 실무에서 시간을 잡아먹습니다. GCC, Clang, MSVC는 표준을 지키지만 경고 메시지, 최적화 성향, 아직 구현되지 않은 기능, 확장 문법에서 갈립니다. 윈도우 장비 소프트웨어와 리눅스 서버를 같은 코드베이스로 지원한다면 CI에서 두 컴파일러를 모두 돌려야 합니다. 템플릿을 쓰기 시작하면 여기에 빌드 시간과 에러 메시지 문제가 붙습니다. 템플릿은 타입마다 따로 실체화되어 코드가 생성되기 때문에, 헤더에 무겁게 들어간 템플릿 하나가 전체 빌드 시간을 끌어올리고, 한 줄 실수가 화면을 가득 채우는 에러로 돌아옵니다. 인터페이스와 구현을 나누고 헤더 의존을 줄이는 작업이 성능 최적화만큼 자주 필요한 이유입니다.
테스트와 관측 도구는 갖춰져 있습니다. GoogleTest나 Catch2로 단위 테스트를 짜고, 주소 검사기와 정의되지 않은 동작 검사기를 켠 빌드를 CI에 하나 더 두고, 메모리 검사 도구로 누수를 훑고, 프로파일러로 실제 병목을 확인합니다. 여기서 순서가 중요합니다. 추측으로 최적화하지 말고 측정부터 한다는 원칙은 C++에서 특히 잘 맞습니다. 대부분의 병목은 알고리즘이나 메모리 접근 패턴에 있지, 눈에 띄는 반복문에 있지 않기 때문입니다. 측정과 개선 흐름은 C++ 성능 가이드와 C++ 테스트 가이드에 정리해 두었습니다.
마지막으로 팀마다 쓰는 C++가 다르다는 점을 각오해야 합니다. 예외를 쓰는 팀과 금지한 팀, 표준 라이브러리를 그대로 쓰는 팀과 자체 컨테이너를 만든 팀, 템플릿을 적극적으로 쓰는 팀과 최소한만 쓰는 팀이 모두 존재합니다. 언어를 배웠다고 해서 새 코드베이스를 바로 읽을 수 있는 것은 아니고, 그 팀이 고른 부분집합과 금지 목록을 먼저 파악하는 것이 첫 주의 일이 됩니다.
지금 배울지 판단하기
아래 조건 중 위쪽에 해당하면 투자할 가치가 있고, 아래쪽에 해당하면 지금은 순서가 아닙니다.
- 제품의 성능이나 지연 시간이 곧 품질 지표인 영역, 즉 장비 제어, 비전, 오디오, 게임 엔진, 시뮬레이션 쪽으로 갈 생각이라면 배울 값을 합니다.
- 이미 회사에 오래된 C++ 코드베이스가 있고 그 코드를 계속 고쳐야 한다면 선택지가 아니라 전제 조건입니다.
- 메모리 배치, 캐시, 시스템 콜 같은 아래층을 이해하고 싶다면, C++로 한 번 겪어 보는 경험이 다른 언어를 쓸 때도 남습니다.
- 지금은 아닙니다 — 첫 언어를 고르는 중이라면 아닙니다. 수명, 빌드, 정의되지 않은 동작이 한꺼번에 오면 프로그래밍 자체를 배우는 데 방해가 됩니다.
- 지금은 아닙니다 — 웹 서비스, 사내 시스템, 데이터 분석처럼 변경 속도가 핵심인 일을 하고 있고 성능 문제가 실제로 터진 적이 없다면 아닙니다.
- 지금은 아닙니다 — 새로 시작하는 시스템 프로그램이고 팀에 기존 C++ 자산이 없다면, 같은 성능대에서 메모리 안전을 언어가 보장해 주는 Rust를 먼저 검토해 볼 만합니다.
- 지금은 아닙니다 — 빌드 환경과 도구를 돌볼 사람이 팀에 없다면, 코드를 쓰기 전에 빌드에서 먼저 막힙니다.
배우기로 했다면 문법을 훑는 것보다 작은 프로그램을 끝까지 만들어 보는 편이 빠릅니다. C++ 예제 모음에서 자료구조와 파일 입출력부터 시작하고, 익숙해지면 패턴 가이드로 넘어가면 됩니다.
인시던트 노트
검사 라인 불량 판정 사고를 정리한 C++ 인시던트 노트
이미지 처리 모델이 아니라 프레임 타이밍이 문제였다. 제조 라인 팀은 무엇을 보고 원인을 좁혔을까.