데니스 리치의 C가 Unix를 어떻게 형성했고 여전히 커널, 임베디드 장치, 고성능 소프트웨어를 구동하는지 — 이식성, 성능, 안전성에서 알아야 할 점을 설명합니다.

C는 대부분 사람들이 직접 다루지 않는 기술 중 하나이지만 거의 모든 사람이 그 혜택을 받고 있는 경우가 많습니다. 휴대폰, 노트북, 라우터, 자동차, 스마트워치, 심지어 디스플레이가 있는 커피머신을 사용한다면 스택 어딘가에 C가 관여해 있을 가능성이 큽니다—기기를 부팅하고 하드웨어와 통신하거나 즉각적으로 느껴질 만큼 충분히 빠르게 실행되도록 하는 역할입니다.
개발자 관점에서 C는 제어력과 이식성의 드문 조합을 제공하기 때문에 실용적인 도구로 남아 있습니다. 메모리와 하드웨어를 직접 관리할 수 있을 만큼 기계에 가깝게 동작하면서도, 비교적 적은 수정으로 다른 CPU와 운영체제에 이식할 수 있습니다. 이런 조합을 대체하기는 어렵습니다.
C의 가장 큰 족적은 세 가지 영역에서 드러납니다:
앱이 상위 수준 언어로 작성되더라도, 기반의 일부(또는 성능 민감 모듈)는 종종 C로 거슬러 올라갑니다.
이 글은 데니스 리치, C의 원래 목표, 그리고 그것이 현대 제품에 여전히 등장하는 이유 사이의 연결고리를 설명합니다. 다룰 내용:
이는 C 그 자체에 관한 글입니다. 모든 저수준 언어를 다루는 것이 아니며, 비교를 위해 C++와 Rust가 언급될 수 있지만 초점은 C가 무엇인지, 왜 그렇게 설계되었는지, 그리고 팀들이 실제 시스템에서 왜 계속 선택하는지에 있습니다.
데니스 리치(1941–2011)는 AT&T의 Bell Labs에서 활동한 미국의 컴퓨터 과학자로, 그곳에서 한창 초기 컴퓨팅과 통신 연구에 기여했습니다.
1960년대 후반과 1970년대의 Bell Labs에서 리치는 Ken Thompson 등과 함께 운영체제 연구를 수행했고, 이 연구는 Unix로 이어졌습니다. Thompson이 초기 Unix를 만들었고, 시스템이 유지·개선·공유되기 쉬운 형태로 발전하는 데 리치가 핵심 공동창작자가 되었습니다.
리치는 Bell Labs에서 쓰이던 이전 언어들에서 아이디어를 발전시켜 C를 만들었습니다. C는 시스템 소프트웨어를 작성하는 데 실용적으로 설계되었는데, 프로그래머가 메모리와 데이터 표현을 직접 제어할 수 있으면서도 어셈블리로 모든 것을 작성하는 것보다 가독성과 이식성이 높습니다.
이 조합은 중요했습니다. Unix는 결국 C로 다시 작성되었는데, 이것은 스타일을 위한 재작성만이 아니었습니다—Unix를 새로운 하드웨어로 옮기고 시간이 지나면서 확장하기 훨씬 쉬워졌습니다. 결과적으로 강력한 피드백 루프가 만들어졌습니다: Unix는 C에 대한 실전적이고 까다로운 사용 사례를 제공했고, C는 Unix를 한 기계에 국한되지 않고 채택하기 쉽게 만들었습니다.
Unix와 C는 함께 우리가 아는 ‘시스템 프로그래밍’을 정의하는 데 일조했습니다: 머신에 가깝지만 특정 프로세서에 묶이지 않는 언어로 운영체제, 핵심 라이브러리, 도구를 만드는 접근법입니다. 이들의 영향은 이후 운영체제, 개발자 도구, 그리고 오늘날 많은 엔지니어들이 배우는 관습에 나타납니다—신화 때문이 아니라 실제로 그 접근법이 대규모로 잘 작동했기 때문입니다.
초기 운영체제들은 대부분 어셈블리로 작성됐습니다. 그 덕분에 엔지니어는 하드웨어를 완벽하게 제어할 수 있었지만, 모든 변경이 느리고 오류가 발생하기 쉬우며 특정 프로세서에 강하게 묶이는 단점이 있었습니다. 작은 기능이라도 저수준 코드 수 페이지가 필요할 수 있었고, 시스템을 다른 머신으로 옮기려면 많은 부분을 다시 써야 했습니다.
데니스 리치는 진공 상태에서 C를 발명한 것이 아닙니다. C는 Bell Labs에서 사용되던 이전의 단순한 시스템 언어들에서 성장했습니다.
C는 메모리의 바이트, 레지스터의 산술, 코드의 점프처럼 컴퓨터가 실제로 하는 것에 깔끔하게 매핑되도록 만들어졌습니다. 그래서 단순한 데이터 타입, 명시적 메모리 접근, CPU 명령에 대응하는 연산자들이 언어의 중심입니다. 대규모 코드베이스를 관리할 수 있을 정도로 고수준이면서도 메모리 레이아웃과 성능을 직접 제어할 수 있을 만큼 직접적입니다.
“이식성”이란 동일한 C 소스 코드를 다른 컴퓨터로 옮겨 최소한의 변경으로 컴파일하면 같은 동작을 얻을 수 있다는 뜻입니다. 운영체제를 각 프로세서에 맞춰 새로 쓰는 대신, 팀은 대부분의 코드를 유지하고 소수의 하드웨어 특화 부분만 교체하면 됩니다. 이 ‘대부분은 공유, 소수는 기계 의존적’이라는 조합이 Unix 확산의 돌파구였습니다.
C의 속도는 마법이 아니라, 코드와 CPU 사이에 불필요한 ‘추가 작업’이 거의 끼어들지 않도록 직접적으로 매핑되는 방식의 결과입니다.
C는 일반적으로 컴파일됩니다. 즉 사람이 읽을 수 있는 소스 코드를 작성하면 컴파일러가 이를 머신 코드로 번역합니다. 컴파일된 결과물은 실행 파일(또는 나중에 링크되어 하나가 되는 오브젝트 파일)입니다. 핵심은 최종 결과가 런타임에 한 줄씩 해석되는 것이 아니라 CPU가 이해하는 형태로 이미 존재한다는 점입니다. 이로 인해 오버헤드가 줄어듭니다.
C는 함수, 루프, 정수, 배열, 포인터 같은 단순한 구성 요소를 제공합니다. 언어가 작고 명시적이기 때문에 컴파일러는 자주 직관적인 머신 코드를 생성할 수 있습니다.
대부분의 경우 백그라운드에서 모든 객체를 추적하거나 숨겨진 체크를 삽입하거나 복잡한 메타데이터를 관리하는 필수 런타임이 없습니다. 루프를 쓰면 대체로 루프가 생성됩니다. 배열 요소에 접근하면 대체로 직접 메모리 접근이 이루어집니다. 이러한 예측 가능성이 C가 성능 민감한 부분에서 잘 동작하는 큰 이유입니다.
C는 수동 메모리 관리를 사용합니다. 프로그램이 명시적으로 메모리를 요청(예: malloc)하고 명시적으로 해제(예: free)합니다. 시스템 수준 소프트웨어는 메모리가 언제 할당되는지, 얼마나 사용되는지, 얼마나 오래 유지되는지를 세밀하게 제어해야 할 때가 많은데, 이는 숨겨진 오버헤드를 최소화하는 데 중요합니다.
대가도 분명합니다: 더 많은 제어는 더 높은 속도와 효율을 가져올 수 있지만 더 많은 책임도 동반합니다. 메모리를 해제하지 않거나 두 번 해제하거나 해제한 메모리를 사용하면 버그는 치명적일 수 있고 때로는 보안에 중대한 영향을 미칩니다.
운영체제는 소프트웨어와 하드웨어의 경계에 놓여 있습니다. 커널은 메모리 관리, CPU 스케줄링, 인터럽트 처리, 장치와의 통신, 그리고 모든 것이 의존하는 시스템 콜 제공을 담당합니다. 이러한 작업은 추상적이지 않습니다—특정 메모리 위치를 읽고 쓰고, CPU 레지스터를 다루고, 불규칙한 시간에 도착하는 이벤트에 반응해야 합니다.
드라이버와 커널은 “정확히 이것을 실행하라”는 것을 표현할 수 있는 언어가 필요합니다. 실제로 이는 다음을 의미합니다:
C는 바이트, 주소, 단순한 제어 흐름을 중심으로 한 모델을 가지므로 이러한 요구에 잘 맞습니다: 필수 런타임, 가비지 컬렉터, 객체 시스템이 부팅 전에 존재해야 하는 부담이 없습니다.
Unix와 초기 시스템 작업은 리치가 형성하는 접근법을 대중화했습니다: 대규모 운영체제의 많은 부분을 이식 가능한 언어로 구현하되, ‘하드웨어 경계’는 얇게 유지합니다. 많은 현대 커널들도 이 패턴을 따릅니다. 어셈블리가 필요한 부분(부트 코드, 컨텍스트 스위치)도 있지만 C가 구현의 대부분을 차지합니다.
C는 표준 C 라이브러리, 핵심 네트워킹 코드, 상위 수준 언어들이 종종 의존하는 저수준 런타임 구성요소 같은 핵심 시스템 라이브러리에서도 지배적입니다. Linux, BSD, macOS, Windows, 또는 RTOS를 사용했다면 의식하든 못하든 C 코드에 의존했을 가능성이 큽니다.
운영체제 작업에서 C의 매력은 향수가 아니라 공학적 경제성에 가깝습니다:
Rust, C++ 등은 운영체제의 일부에서 사용되며 실질적인 이점을 제공할 수 있습니다. 그럼에도 C는 공통 분모로 남아 있습니다: 많은 커널이 C로 작성되어 있고, 대부분의 저수준 인터페이스는 C를 가정하며, 다른 시스템 언어들이 상호운용해야 하는 기준입니다.
“임베디드”는 보통 우리가 컴퓨터라고 생각하지 않는 기기들을 가리킵니다: 온도조절기 내부의 마이크로컨트롤러, 스마트 스피커, 라우터, 자동차, 의료기기, 공장 센서, 수많은 가전제품 등. 이런 시스템은 종종 수년간 단일 목적을 조용히 수행하며 비용·전력·메모리 제약이 엄격합니다.
많은 임베디드 타깃은 킬로바이트(기가바이트가 아님)의 RAM과 제한된 플래시 저장공간을 가집니다. 일부는 배터리로 작동하며 대부분의 시간 동안 슬립해야 합니다. 다른 일부는 실시간 데드라인을 가지며—모터 제어 루프가 몇 밀리초 늦어지면 하드웨어가 잘못 동작할 수 있습니다.
이런 제약은 프로그램의 크기, 깨어나는 빈도, 타이밍 예측 가능성 등 모든 결정을 규정합니다.
C는 런타임 오버헤드가 거의 없는 작은 바이너리를 만드는 경향이 있습니다. 필수 가상 머신이 없고 동적 할당을 전혀 피할 수 있는 경우도 많습니다. 이는 펌웨어를 고정된 플래시 크기에 맞추거나 장치가 예기치 않게 ‘일시 중지’하지 않도록 보장해야 할 때 중요합니다.
같이 중요한 점은 C가 하드웨어와 통신하기 쉬운 모델을 제공한다는 것입니다. 임베디드 칩은 메모리 맵 레지스터를 통해 주변 장치(GPIO, 타이머, UART/SPI/I2C 등)를 노출합니다. C의 모델은 이를 자연스럽게 매핑합니다: 특정 주소를 읽고 쓰고, 개별 비트를 제어하고, 거의 추상화 없이 이를 수행할 수 있습니다.
많은 임베디드 C 코드가 다음 중 하나입니다:
어쨌든 하드웨어 레지스터(volatile로 표시되는 경우가 많음), 고정 크기 버퍼, 신중한 타이밍을 중심으로 한 코드가 보입니다. 그 ‘기계에 가까운’ 스타일이 바로 C가 메모리·전력·신뢰성 제약이 있는 펌웨어의 기본 선택으로 남는 이유입니다.
“성능 중요”란 시간과 자원이 제품의 일부일 때를 뜻합니다: 밀리초가 UX에 영향을 주고, CPU 사이클이 서버 비용에 영향을 주며, 메모리 사용량이 프로그램이 맞느냐 못 맞느냐를 가릅니다. 이런 곳에서 C는 데이터 레이아웃, 작업 스케줄링, 컴파일러가 최적화할 수 있는 범위를 제어하게 해주기 때문에 여전히 기본 옵션입니다.
다음과 같은 시스템의 핵심에서 C를 자주 찾습니다:
이들 도메인은 전체가 ‘빠른’ 것은 아닙니다. 보통 특정 내부 루프가 런타임을 지배합니다.
팀들은 전체 제품을 C로 다시 쓰지 않습니다. 대신 프로파일링으로 핫 패스(시간의 대부분이 소비되는 작은 코드 부분)를 찾아 최적화합니다.
C는 핫 패스에 유리합니다. 이유는 핫 패스가 종종 메모리 접근 패턴, 캐시 동작, 분기 예측, 할당 오버헤드 같은 저수준 세부사항에 의해 제약받기 때문입니다. 데이터 구조를 튜닝하고 불필요한 복사를 피하며 할당을 제어하면 나머지 코드를 건드리지 않고도 속도 향상이 극적으로 나타날 수 있습니다.
현대 제품은 종종 다중 언어로 구성됩니다: 대부분 코드는 Python, Java, JavaScript, 또는 Rust로 작성하고 핵심은 C로 둡니다.
일반적 통합 방식:
이 모델은 실용적입니다: 상위 수준 언어에서는 빠르게 반복하고, 성능이 필요한 곳에서는 예측 가능한 성능을 유지합니다. 단점은 경계에서의 데이터 변환, 소유권 규칙, 오류 처리에 신경 써야 한다는 점입니다.
C가 빠르게 퍼진 이유 중 하나는 ‘여기저기서 실행될 수 있다’는 점입니다: 동일한 핵심 언어가 작은 마이크로컨트롤러부터 초거대 컴퓨터까지 넓은 범위의 머신에 구현될 수 있습니다. 이 이식성은 마법이 아니라—공유된 표준과 표준을 따르는 문화의 결과입니다.
초기 C 구현은 벤더마다 달라 코드 공유가 어려웠습니다. 큰 전환점은 ANSI C(C89/C90로 불림)와 이후의 ISO C(C99, C11, C17, C23 등)였습니다. 숫자를 외울 필요는 없고, 중요한 점은 표준이 언어와 표준 라이브러리가 어떻게 동작하는지에 대한 공개된 합의라는 것입니다.
표준은 다음을 제공합니다:
표준을 염두에 둔 코드는 컴파일러와 플랫폼을 바꿔도 놀랄 만큼 적은 변경으로 이동할 수 있습니다.
이식성 문제는 보통 표준이 보장하지 않는 것들에 의존할 때 발생합니다. 예로는:
int가 반드시 32비트라고 보장되지 않으며 포인터 크기도 달라집니다. 크기를 가정하면 타깃을 바꿀 때 실패할 수 있습니다.좋은 기본 전략은 표준 라이브러리를 우선 사용하고 비이식적인 코드는 작은 명확한 래퍼 뒤에 숨기는 것입니다.
또한 이식성과 정의된 동작을 강화하는 컴파일 플래그로 빌드하세요. 흔한 선택 예:
-std=c11)-Wall -Wextra)표준 우선 코드와 엄격한 빌드는 어떤 ‘영리한’ 트릭보다 이식성에 더 큰 도움을 줍니다.
C의 힘은 동시에 날카로운 칼날입니다: 메모리에 가깝게 작업할 수 있게 해 주지만, 다른 언어가 막아주는 실수를 초래하기 쉽습니다. 이는 C가 빠르고 유연한 이유이면서 초보자(또는 피로한 전문가)가 실수하기 쉬운 이유이기도 합니다.
프로그램의 메모리를 번호가 매겨진 우편함이 줄지어 있는 거리로 상상해 보세요. 변수는 무언가(예: 정수)를 담고 있는 상자입니다. 포인터는 그 물건 자체가 아니라 ‘어떤 상자를 열어야 하는지 적힌 쪽지’와 같습니다. 즉 포인터는 그 상자의 주소를 가리킵니다.
이건 유용합니다: 값을 복사하지 않고 주소를 전달할 수 있고 배열, 버퍼, 구조체, 심지어 함수도 가리킬 수 있습니다. 하지만 주소가 틀리면 잘못된 상자를 열게 됩니다.
이 문제들은 충돌, 무언의 데이터 손상, 보안 취약점으로 이어질 수 있습니다. 시스템 코드(종종 C로 작성됨)에서는 이러한 실패가 그 위의 모든 것에 영향을 미칠 수 있습니다.
C가 ‘기본적으로 안전하지 않다’고 말하기보다는, C는 관대하다고 보는 편이 맞습니다: 컴파일러는 당신이 쓴 것을 그대로 의미한다고 가정합니다. 이는 성능과 저수준 제어에 유리하지만, 신중한 습관·리뷰·좋은 도구와 함께 쓰지 않으면 오용되기 쉽습니다.
C는 직접 제어를 주지만 실수를 잘 용서하지 않습니다. 다행히도 “안전한 C”는 마법이 아니라 규율화된 습관, 명확한 인터페이스, 그리고 도구에 맡기는 것입니다.
잘못된 사용을 어렵게 만드는 API를 설계하세요. 포인터와 함께 버퍼 크기를 받는 함수를 선호하고, 명시적 상태코드 반환과 메모리 소유권을 문서화하세요.
경계 검사(bounds checking)는 예외가 아니라 일상이어야 합니다. 함수가 버퍼에 쓸 때는 길이를 사전에 검증하고 실패 시 즉시 에러를 반환하세요. 메모리 소유권은 단순하게 유지하세요: 하나의 할당자, 하나의 대응 해제 경로, 호출자와 피호출자 중 누가 해제 책임이 있는지에 대한 명확 규칙.
현대 컴파일러는 위험한 패턴에 대해 경고할 수 있습니다—CI에서 경고를 오류로 취급하세요. AddressSanitizer, UBSan 같은 런타임 검사기를 개발 중에 사용해 경계 초과 쓰기, 사용 후 참조, 정수 오버플로우 등을 발견하세요.
정적 분석기와 린터는 테스트에서 드러나지 않는 문제를 찾는 데 도움됩니다. 퍼징은 파서와 프로토콜 핸들러에 특히 효과적입니다: 예상치 못한 입력을 생성해 버퍼와 상태기계 버그를 자주 드러냅니다.
코드 리뷰에서는 오프바이원 인덱싱, 누락된 NUL 종료자, 부호/부호 없음 혼용, 검사하지 않은 반환값, 에러 경로에서의 메모리 누수 등 C의 흔한 실패 모드에 대해 명시적으로 살펴보세요.
언어가 보호해 주지 않을 때는 테스트가 더 중요합니다. 단위 테스트는 좋고, 통합 테스트는 더 낫고, 이전에 발견된 버그에 대한 회귀 테스트가 가장 낫습니다.
엄격한 신뢰성이나 안전 요건이 있다면 C의 제한된 ‘부분집합’과 문서화된 규칙(예: 포인터 산술 제한, 특정 라이브러리 호출 금지, 래퍼 요구)을 채택하세요. 핵심은 일관성입니다: 도구와 리뷰로 강제할 수 있는 규칙을 선택하세요. 슬라이드 위의 이상이 아니라 실제로 지킬 수 있는 규칙을.
C는 메모리, 데이터 레이아웃, 하드웨어 접근 같은 저수준 제어와 광범위한 이식성을 결합하기 때문에 여전히 중요합니다. 이 조합은 머신 부팅, 제한된 환경에서의 실행, 또는 예측 가능한 성능이 필요한 코드에 실용적인 선택이 됩니다.
C는 다음 영역에서 여전히 지배적입니다:
대부분의 애플리케이션이 상위 수준 언어로 작성되더라도, 중요한 기반 부분은 종종 C에 의존합니다.
데니스 리치는 Bell Labs에서 시스템 소프트웨어를 실용적으로 쓰기 위해 C를 만들었습니다: 기계에 가깝지만 어셈블리보다 이식성과 유지보수성이 높은 언어였습니다. 결정적 사례는 Unix를 C로 다시 작성한 것으로, 이는 Unix를 새로운 하드웨어로 옮기고 확장하기 쉽게 만들었습니다.
단순히 말해, 이식성이란 동일한 C 소스 코드를 다른 CPU/운영체제에서 최소한의 변경으로 컴파일해 일관된 동작을 얻을 수 있다는 의미입니다. 보통 대부분의 코드는 공유하고 하드웨어/OS 특화 부분만 작은 모듈로 가둡니다.
C가 빠른(또는 더 예측 가능한) 이유는 기계 연산에 가깝게 매핑되고 보통 필수 런타임 오버헤드가 적기 때문입니다. 컴파일러는 루프, 산술, 메모리 접근에 대해 직관적인 머신 코드를 생성하는 경우가 많고, 이는 밀집된 내부 루프에서 성능에 큰 이득을 줍니다.
많은 C 프로그램은 수동 메모리 관리를 사용합니다:
malloc)free)이는 메모리를 언제 얼마나 얼마나 쓸지 정확히 제어할 수 있게 해 주며, 커널·임베디드·핫패스 같은 곳에서 유용합니다. 대가로 실수는 충돌이나 보안 문제를 초래할 수 있습니다.
커널과 드라이버는 다음을 필요로 합니다:
C는 안정된 툴체인과 예측 가능한 바이너리를 제공하면서 이러한 요구를 잘 맞춥니다.
임베디드 대상은 종종 아주 작은 RAM/플래시 예산, 엄격한 전력 한계, 실시간 요구를 가집니다. C는 작은 바이너리를 생성하고 런타임 오버헤드를 피할 수 있으며 메모리 맵 레지스터와 인터럽트를 통해 하드웨어와 직접 통신하기 쉬워 이런 제약에 잘 맞습니다.
일반적인 접근은 애플리케이션의 대부분을 상위 수준 언어로 두고 성능에 민감한 핫 패스만 C로 작성하는 것입니다. 통합 옵션에는:
경계에서는 데이터 변환, 소유권 규칙, 오류 처리에 주의해야 합니다.
실무에서 ‘더 안전한 C’는 규율과 도구의 조합입니다:
-Wall -Wextra)로 컴파일하고 경고를 진지하게 다루기이로써 흔한 버그는 크게 줄일 수 있습니다.