‘시스템 프로그래밍’이라는 말이 왜 저수준 프로그래밍과 시스템 설계를 뒤섞어 왔는지 살펴보고, 그 구분을 다시 정의하면 무엇을 얻을 수 있는지 역사적 맥락 속에서 고찰한다.
URL: https://willcrichton.net/notes/systems-programming/
Will Crichton — 2018년 9월 9일
나는 “시스템 프로그래밍(systems programming)”이라는 표현에 늘 불만이 있다. 내게 이 말은 불필요하게 두 가지 개념을 하나로 묶어버리는 것처럼 보인다. 하나는 저수준 프로그래밍(기계의 구현 세부사항을 다루는 것)이고, 다른 하나는 시스템 설계(서로 맞물려 동작하는 복잡한 구성요소들의 집합을 만들고 관리하는 것)다. 왜 이런 일이 생겼을까? 언제부터 이런 의미가 사실이었을까? 그리고 ‘시스템’이라는 개념을 다시 정의한다면 무엇을 얻을 수 있을까?
용어가 어떻게 발전했는지 이해하기 위해 현대 컴퓨터 시스템의 기원으로 돌아가 보자. 누가 처음 이 표현을 만들었는지는 모르겠지만, 내가 찾아본 바로는 “컴퓨터 시스템”을 정의하려는 본격적인 시도가 70년대 초반 무렵부터 시작된 듯하다. Systems Programming Languages (Bergeron1 외, 1972)에서 저자들은 이렇게 말한다.
시스템 프로그램(system program)이란 서로 통합된 하위 프로그램들의 집합으로, 함께 하나의 전체를 이루며 그 전체는 부분의 합보다 더 크고, 일정 수준의 규모 및/또는 복잡성의 임계치를 넘는다. 전형적인 예로는 다중 프로그래밍, 번역(컴파일), 시뮬레이션, 정보 관리, 시분할(time sharing) 시스템 등이 있다. […] 아래는 일부 속성들의 부분 집합으로, 그 중 일부는 비-시스템에도 존재할 수 있으며, 어떤 속성들은 특정 시스템에 반드시 모두 존재할 필요는 없다.
- 해결해야 할 문제는 광범위하며, 많은(대개 매우 다양한) 하위 문제들로 구성된다.
- 시스템 프로그램은 다른 소프트웨어와 응용 프로그램을 지원하기 위해 사용될 가능성이 크지만, 그 자체로 완전한 응용 패키지일 수도 있다.
- 단발성(one-shot) 해결책이 아니라 지속적인 “프로덕션” 사용을 위해 설계된다.
- 지원하는 기능의 수와 종류 측면에서 지속적으로 진화할 가능성이 크다.
- 시스템 프로그램은 모듈 내부 및 모듈 간(즉, “통신”) 모두에서 일정한 규율 또는 구조를 필요로 하며, 보통 한 사람 이상이 설계·구현한다.
이 정의는 상당히 수긍이 간다. 컴퓨터 시스템은 대규모이고, 오랫동안 사용되며, 시간에 따라 변한다. 하지만 이 정의가 대체로 기술적(descriptive)인 반면, 이 논문에서 핵심 아이디어 하나는 규범적(prescriptive)이다. 즉 저수준 언어와 시스템 언어를 분리해야 한다고 주장한다(당시에는 어셈블리와 FORTRAN을 대비시키는 맥락이었다).
시스템 프로그래밍 언어의 목표는 “비트 만지작(bit twiddling)” 같은 고려에 과도하게 신경 쓰지 않고도 사용할 수 있으면서, 손으로 짠 코드에 비해 눈에 띄게 나쁘지 않은 코드를 생성하는 언어를 제공하는 것이다. 그런 언어는 고급 언어의 간결함과 가독성에 더해, 어셈블리 언어에서 얻을 수 있는 공간·시간 효율성과 기계 및 운영체제 기능에 “접근할 수 있는” 능력을 결합해야 한다. 설계·작성·디버깅 시간은 최소화하되, 시스템 자원에 불필요한 오버헤드를 강요해서는 안 된다.
같은 시기 CMU 연구자들은 BLISS: A Language for Systems Programming (Wulf 외, 1972)라는 논문을 발표하며 BLISS를 이렇게 설명한다.
우리는 BLISS를 “구현 언어(implementation language)”라고 부르는데, 물론 이 용어는 다소 모호하다는 것도 인정한다. 아마 모든 컴퓨터 언어는 뭔가를 구현하는 데 쓰이기 때문이다. 그러나 우리에게 이 표현은 범용적이고 더 높은 수준의 언어로서, 특정 응용(즉 특정 기계를 위한 크고, 프로덕션용 소프트웨어 시스템을 작성하는 일)에 주된 강조점을 둔 언어를 뜻한다. 컴파일러-컴파일러 같은 특수 목적 언어는 이 범주에 들지 않으며, 또한 이런 언어가 반드시 기계 독립적일 필요도 없다고 본다. 우리는 정의에서 “구현”이라는 단어를 강조하며 “설계”나 “문서화” 같은 단어를 사용하지 않았다. 구현 언어가 대형 시스템의 초기 설계를 표현하기에 적합한 수단이거나, 혹은 그 시스템의 전용 문서화 수단이 되리라고 반드시 기대하지는 않는다. 기계 독립성, 설계와 구현을 동일한 표기법으로 표현하는 것, 자기 문서화(self-documentation) 등은 분명 바람직한 목표이며, 우리는 여러 언어를 평가할 때 이런 것들을 기준으로 삼았다.
여기서 저자들은 “구현 언어”가 어셈블리보다 고수준이지만 “설계 언어”보다는 저수준이라는 점을 대비시킨다. 이는 앞선 논문과는 다른 입장으로, 시스템을 설계하는 언어와 시스템을 구현하는 언어는 분리되어야 한다고 주장한다.
이 두 논문은 연구 성과물이자 일종의 주창(advocacy)이다. 마지막으로 고려할 (역시 1972년, 생산적인 해였다!) 항목은 Systems Programming (Donovan 1972)으로, 시스템 프로그래밍을 배우기 위한 교육용 텍스트다.
시스템 프로그래밍이란 무엇인가? 컴퓨터를 어떤 명령이든 다 따르는 일종의 짐승으로 상상할 수도 있다. 컴퓨터는 기본적으로 금속으로 만들어진 사람이라거나, 반대로 사람은 살과 피로 만들어진 컴퓨터라고 말하기도 한다. 하지만 컴퓨터에 가까이 다가가 보면, 컴퓨터는 기본적으로 매우 구체적이고 원시적인 명령을 따르는 기계임을 알 수 있다. 컴퓨터의 초기에는 원시적인 명령을 나타내는 on 과 off 스위치로 사람과 컴퓨터가 소통했다. 곧 사람들은 더 복잡한 명령을 주고 싶어했다. 예컨대 Y = 10일 때 X = 30 * Y; 라고 말하고 싶었던 것이다. 오늘날의 컴퓨터는 시스템 프로그램의 도움 없이는 이런 언어를 이해하지 못한다. 시스템 프로그램(예: 컴파일러, 로더, 매크로 프로세서, 운영체제)은 컴퓨터가 사용자의 필요에 더 잘 맞도록 개발되었다. 더 나아가 사람들은 프로그램을 준비하는 메커니즘에서도 더 많은 도움을 원했다.

이 정의가 좋은 점은, 시스템이 사람을 위해 존재한다는 사실을 상기시킨다는 것이다. 비록 시스템이 최종 사용자에게 직접 노출되지 않는 인프라일 뿐이라 하더라도 말이다.
70~80년대에는 대부분의 연구자들이 시스템 프로그래밍을 대체로 어셈블리 프로그래밍과 대비되는 것으로 본 듯하다. 시스템을 만들 수 있는 다른 좋은 도구가 없었다. (이 모든 흐름에서 Lisp가 어디 있었는지는 확신이 없다. 내가 읽은 자료들 중 Lisp를 인용한 것은 없었지만, Lisp 머신이 잠깐 존재했다는 정도는 어렴풋이 알고 있다.)
하지만 90년대 중반, 동적 타입 스크립팅 언어가 부상하면서 프로그래밍 언어 지형에 큰 변화가 일어났다. Bash 같은 초기 셸 스크립팅 시스템을 개선하며 Perl(1987), Tcl(1988), Python(1990), Ruby(1995), PHP(1995), Javascript(1995) 같은 언어들이 주류로 자리 잡기 시작했다. 이는 영향력 있는 글 “Scripting: Higher Level Programming for the 21st Century” (Ousterhout 1998)에서 정점을 이룬다. 이 글은 “시스템 프로그래밍 언어”와 “스크립팅 언어” 사이의 “Ousterhout의 이분법”을 명확히 했다.
스크립팅 언어는 시스템 프로그래밍 언어와는 다른 과업을 위해 설계되었고, 이는 언어 사이에 근본적 차이를 낳는다. 시스템 프로그래밍 언어는 메모리 워드 같은 가장 원시적인 컴퓨터 요소들로부터 시작해 데이터 구조와 알고리즘을 처음부터 구축하기 위해 설계되었다. 반면 스크립팅 언어는 접착(gluing)을 위해 설계되었다. 즉 강력한 구성요소들의 집합이 이미 존재한다고 가정하고, 주로 구성요소들을 서로 연결하기 위한 목적으로 의도된다. 시스템 프로그래밍 언어는 복잡성 관리를 위해 강한 타입 시스템을 갖는 반면, 스크립팅 언어는 구성요소 간 연결을 단순화하고 빠른 애플리케이션 개발을 제공하기 위해 무타입(typeless)이다. […] 더 빠른 기계, 더 나은 스크립팅 언어, GUI와 컴포넌트 아키텍처의 중요성 증가, 인터넷의 성장 같은 최근의 여러 추세는 스크립팅 언어의 적용 가능성을 크게 높였다.

기술적 관점에서 Ousterhout는 위 그림처럼 타입 안정성과 문장당 명령 수(instructions-per-statement)라는 축을 따라 스크립팅과 시스템을 대비시켰다. 설계 관점에서는 각 언어 부류의 새로운 역할을 규정했다. 시스템 프로그래밍은 구성요소를 만드는 것이고, 스크립팅은 그것들을 접착해 연결하는 것이다.
비슷한 시기, 정적 타입이지만 가비지 컬렉션을 사용하는 언어들도 인기를 얻기 시작했다. Java(1995)와 C#(2000)은 오늘날 우리가 아는 거대한 존재가 되었다. 이 둘은 전통적으로 “시스템 프로그래밍 언어”로 보진 않지만, 세계 최대 규모의 소프트웨어 시스템들을 설계하는 데 많이 사용되어 왔다. Ousterhout는 심지어 “지금 형성되고 있는 인터넷 세계에서 Java는 시스템 프로그래밍에 사용된다”고 명시적으로 언급하기도 했다.
지난 10년 동안 스크립팅 언어와 시스템 프로그래밍 언어 사이의 경계는 흐려지기 시작했다. Dropbox 같은 회사는 Python만으로도 놀라울 정도로 크고 확장 가능한 시스템을 만들었다. Javascript는 수십억 개의 웹 페이지에서 실시간으로 복잡한 UI를 렌더링하는 데 쓰인다. 점진적 타이핑(gradual typing)은 Python, Javascript 등 여러 스크립팅 언어에서 탄력을 받았고, 정적 타입 정보를 조금씩 추가함으로써 “프로토타입” 코드에서 “프로덕션” 코드로 전환할 수 있게 해 주었다.
동시에 정적 언어(예: Java의 HotSpot)와 동적 언어(예: Lua의 LuaJIT, Javascript의 V8, Python의 PyPy) 모두에 대한 JIT 컴파일러에 막대한 엔지니어링 자원이 투입되면서, 이들의 성능이 전통적 시스템 프로그래밍 언어(C, C++)와 견줄 만한 수준이 되었다. Spark 같은 대규모 분산 시스템은 Scala2로 작성되어 있다. Julia, Swift, Go 같은 새로운 언어들은 가비지 컬렉션을 사용하는 언어들에서 성능의 경계를 계속 밀어붙이고 있다.
Systems Programming in 2014 and Beyond라는 패널에는 오늘날 스스로를 시스템 언어라고 규정하는 언어들의 핵심 인물들이 참여했다. Bjarne Stroustrup(C++ 창시자), Rob Pike(Go 창시자), Andrei Alexandrescu(D 개발자), Niko Matsakis(Rust 개발자)다. “2014년에 시스템 프로그래밍 언어란 무엇인가”라는 질문에 그들은 이렇게 답했다(전사본을 편집함).
- Niko Matsakis: 클라이언트 측 애플리케이션을 작성하는 것. Go가 설계된 방향과는 정반대다. 이런 애플리케이션에서는 높은 지연(latency) 요구, 높은 보안 요구, 서버 측에는 등장하지 않는 다양한 요구사항이 있다.
- Bjarne Stroustrup: 시스템 프로그래밍은 하드웨어를 다뤄야 했던 분야에서 나왔고, 그 다음 애플리케이션이 더 복잡해졌다. 복잡성을 다룰 필요가 있다. 의미 있는 자원 제약 문제가 있다면, 당신은 시스템 프로그래밍 영역에 있다. 더 미세한 제어가 필요해도 시스템 프로그래밍 영역이다. 시스템 프로그래밍인지를 결정하는 건 제약이다. 메모리가 부족한가? 시간이 부족한가?
- Rob Pike: 우리가 Go를 처음 발표했을 때 시스템 프로그래밍 언어라고 불렀고, 나는 그 점을 약간 후회한다. 많은 사람들이 운영체제를 작성하는 언어라고 가정했기 때문이다. 우리가 그렇게 불렀어야 했던 건 서버 작성 언어였고, 그게 우리가 실제로 생각했던 바다. 이제는 우리가 가진 것이 클라우드 인프라 언어라는 걸 이해한다. 시스템 프로그래밍의 또 다른 정의는 클라우드에서 돌아가는 것들이다.
- Andrei Alexandrescu: 어떤 것이 시스템 프로그래밍 언어인지 확인하기 위한 리트머스 테스트가 몇 가지 있다. 시스템 프로그래밍 언어라면 그 언어로 직접 메모리 할당자(allocator)를 작성할 수 있어야 한다. 숫자를 포인터로 만들어낼 수 있어야 한다. 하드웨어가 그렇게 동작하기 때문이다.
그렇다면 시스템 프로그래밍은 고성능에 관한 것일까? 자원 제약? 하드웨어 제어? 클라우드 인프라? 대체로 C, C++, Rust, D 같은 범주의 언어들은 기계로부터의 추상화 수준이 다르다는 점에서 구별되는 듯하다. 이런 언어들은 메모리 할당/레이아웃과 미세한 자원 관리 같은 하위 하드웨어의 세부사항을 노출한다.
다른 관점도 있다. 효율성 문제가 있을 때, 그것을 해결할 자유가 얼마나 있는가? 저수준 프로그래밍 언어의 멋진 점은 비효율을 발견했을 때, 기계의 세부사항을 정교하게 제어함으로써 병목을 제거하는 것이 내 능력 범위 안에 있다는 것이다. 이 명령을 벡터화하고, 저 데이터 구조를 캐시에 맞게 크기 조정하고, 등등. 정적 타입이 “내가 더하려는 이 두 값은 확실히 정수다” 같은 더 큰 확신3을 제공하듯, 저수준 언어는 “이 코드는 내가 지정한 대로 기계에서 실행될 것이다”라는 _더 큰 확신_을 제공한다.
반대로 인터프리터 언어를 최적화하는 일은 완전한 정글이다. 런타임이 내 기대대로 일관되게 코드를 실행할지 알기 매우 어렵다. 이는 자동 병렬화 컴파일러의 문제와 정확히 같다. “자동 벡터화는 프로그래밍 모델이 아니다”(참고: The story of ispc). Python에서 인터페이스를 작성해 놓고 “이 함수를 호출하는 누군가가 정수를 주겠지 뭐”라고 생각하는 것과 비슷하다.
다시 처음의 불만으로 돌아온다. 많은 사람들이 시스템 프로그래밍이라 부르는 것을, 나는 그저 저수준 프로그래밍—기계의 세부사항을 드러내는 것—이라고 생각한다. 그렇다면 시스템은 무엇인가? 1972년 정의를 다시 떠올려 보자.
- 해결해야 할 문제는 광범위하며, 많은(대개 매우 다양한) 하위 문제들로 구성된다.
- 시스템 프로그램은 다른 소프트웨어와 응용 프로그램을 지원하기 위해 사용될 가능성이 크지만, 그 자체로 완전한 응용 패키지일 수도 있다.
- 단발성(one-shot) 해결책이 아니라 지속적인 “프로덕션” 사용을 위해 설계된다.
- 지원하는 기능의 수와 종류 측면에서 지속적으로 진화할 가능성이 크다.
- 시스템 프로그램은 모듈 내부 및 모듈 간(즉, “통신”) 모두에서 일정한 규율 또는 구조를 필요로 하며, 보통 한 사람 이상이 설계·구현한다.
이것들은 저수준 성능 이슈라기보다 소프트웨어 공학 이슈(모듈성, 재사용, 코드 진화)에 훨씬 가깝다. 그렇다면 이런 문제들을 해결하는 데 우선순위를 두는 어떤 프로그래밍 언어라도 시스템 프로그래밍 언어라고 할 수 있다! 그렇다고 해서 모든 언어가 시스템 프로그래밍 언어라는 뜻은 아니다. 동적 프로그래밍 언어는 여전히 시스템 언어와는 거리가 멀다고 볼 수 있다. 동적 타입과 “허락보다 용서를 구하라(ask forgiveness, not permission)” 같은 관용구는 좋은 코드 품질에 도움이 되지 않기 때문이다.
그렇다면 이 정의가 우리에게 주는 것은 무엇일까? 여기 내 (논쟁적인) 주장 하나가 있다. OCaml과 Haskell 같은 함수형 언어가 C나 C++ 같은 저수준 언어보다 훨씬 더 시스템 지향적이다. 학부생에게 시스템 프로그래밍을 가르칠 때, 불변성(immutability)의 가치, 풍부한 타입 시스템이 인터페이스 설계를 개선하는 효과, 고차 함수의 유용성 같은 함수형 프로그래밍 원리도 포함해야 한다. 학교는 시스템 프로그래밍과 저수준 프로그래밍을 둘 다 가르쳐야 한다.
이 주장을 따른다면, 시스템 프로그래밍과 좋은 소프트웨어 공학 사이에 구분이 있기는 한가? 사실상 없다. 하지만 문제는 소프트웨어 공학과 저수준 프로그래밍이 종종 서로 고립된 채로 가르쳐진다는 점이다. 대부분의 소프트웨어 공학 수업이 Java 중심으로 “좋은 인터페이스와 테스트를 작성하라”에 머문다면, 우리는 자원 제약이 큰 시스템을 어떻게 설계하는지도 학생들에게 가르쳐야 한다. 아마 저수준 프로그래밍을 “시스템”이라고 부르는 이유는 가장 흥미로운 소프트웨어 시스템들(예: 데이터베이스, 네트워크, 운영체제 등) 다수가 저수준이기 때문일 것이다. 저수준 시스템은 제약이 많기 때문에, 설계자는 창의적으로 생각할 수밖에 없다.
또 다른 틀은 이렇다. 저수준 프로그래머는 시스템 설계의 아이디어 중 어떤 것들이 현대 하드웨어의 현실을 다루는 데 адап트될 수 있는지 이해하려고 해야 한다. 나는 Rust 커뮤니티가 이 점에서 대단히 혁신적이었다고 생각한다. 좋은 소프트웨어 설계/함수형 프로그래밍 원리를 저수준 문제에 적용하는 방법을 찾아왔기 때문이다(예: futures, 에러 처리, 그리고 물론 메모리 안전성).
요약하자면, 우리가 “시스템 프로그래밍”이라 부르는 것은 “저수준 프로그래밍”이라고 불러야 한다고 나는 생각한다. 컴퓨터 시스템 설계는 너무 중요한 분야라서, 그 자체의 이름이 있어야 한다. 이 두 개념을 명확히 분리하면 프로그래밍 언어 설계 공간을 더 또렷하게 이해할 수 있고, 두 공간 사이에서 통찰을 공유할 문도 열린다. 기계 주변의 시스템을 어떻게 설계할 것인가, 그리고 그 반대는 어떠한가?
댓글은 내 메일함(wcrichto@cs.stanford.edu)이나 Hacker News로 보내 달라.