최근 등장한 시스템 프로그래밍 언어들의 흐름과, 이 언어들이 안전성·성능·표현력의 균형을 어떻게 추구하는지, 그리고 이를 위해 사용하는 주요 기법들을 간단히 정리한 글입니다.
지난 몇 년 동안 개발 중인 새로운 시스템 언어의 수가 폭발적으로 늘어났다. 이들 대부분은 안전성, 성능, 표현력 사이에서 좋은 균형점을 찾으려 한다. 이 글에서는 먼저 “안전성”이 무엇을 뜻하는지 간단히 개괄하고, 오늘날의 시스템 언어들이 이를 어떻게 달성하려 하는지 조금 살펴보겠다.
오늘 당장 사용할 수 있는 주목할 만한 새 언어들은 다음과 같다: Rust, Zig, Odin, Jakt, Hare
다음 그룹은 다소 실험적인 수준부터 매우 실험적인 수준까지 걸쳐 있지만 그래도 시도해 볼 수는 있다: Vale, Austral, Myrddin, V, Lobster, Compis, Cone
이들은 시스템 언어의 두 번째로 최근 세대에 속한다. 모두 애플리케이션 개발 쪽에 조금 더 가깝고, 고도로 최적화된 메모리 관리에는 덜 초점을 둔다. 전부 1.0 단계를 한참 넘겼고 실제 소프트웨어에서 널리 사용된다: Nim, Crystal, D, Go
각 언어에 대한 세부 내용과 내 생각은 후속 글에서 다룰 계획이다. 모두 지금 사용할 수는 있지만, 전부가 초기 실험 단계를 벗어난 것은 아니다.
새로운 시스템 언어들은 오래되고 확립된 언어들에 비해 무엇을 제공할까? 주로 넓은 의미의 “안전성”이고, 그다음으로는 문법적 편의성이다. 고급 메모리 관리 외에는 대체로 새로운 프로그래밍 언어 개념을 탐구하려 하지 않는다. 그렇다고 해도 C, C++, Fortran과 비교하면 합 타입 같은 함수형 언어의 혁신을 차용했다.
새로운 흐름의 시스템 언어들은 보통 훨씬 더 제정신인 빌드 시스템, 라이브러리 / 의존성 관리, IDE 통합도 제공하지만 그건 다른 글의 주제다.
안전성에는 다음이 포함된다: 메모리 누수와 이중 해제 오류 피하기, 버퍼 오버플로 방지, 동시성 버그 가능성 최소화, 자동 자원 관리, 그리고 잘못된 출력이나 프로그램 충돌로 이어지는 위험한 패턴을 피하도록 전반적으로 설계하는 것. 더 안전한 언어일수록 보안 취약점이 더 적겠지만, 보안은 안전성과는 별개의 문제다. 프로그램 보안을 강화하려면 추가적인 조치가 필요하다.
연구와 경험은 공유되고 변경 가능한 상태가 많은 비안전한 상황의 중심점이라는 사실을 보여 주었다. 즉, 이미 존재하는 버그이거나 사소한 코드 변경 이후 곧 버그가 될 수 있는 것들이다. 따라서 Rust와 많은 현대 시스템 언어는 여러 기법을 통해 이를 최소화하려 한다.
공유된 변경 가능 상태는 동시 실행 중에 가장 흔히 발생할 수 있다. 따라서 어떤 언어가 동시성을 금지하지 않는다면, 공유된 변경 가능 상태를 제한해야 한다. 그렇게 하면 전반적으로 더 안전한 프로그램이 된다. 공유된 변경 가능 상태를 줄이는 것은 “시간적” 메모리 안전성의 한 형태다.
메모리 안전성에는 공간적 차원과 시간적 차원이 모두 있다. 공간적 안전성 기능의 예로는 배열 경계 검사(array bounds checking)가 있다. 예를 들어 배열 끝을 넘어선 메모리를 프로그램이 읽거나 쓰지 못하게 막는다. 시간적 안전성은 프로그램이 실행되는 동안 시간의 흐름에 따라 메모리 사용을 추적하는 것과 관련되며, 메모리 영역이 잘못 사용되거나 잘못 재사용되지 않도록 보장한다. 시간적 메모리 버그의 가장 단순한 예는 나중에 사용하기 전에 변수를 초기화하지 않는 것이다.
초기화되지 않은 변수를 막는 것은 새로운 언어에서 설계적으로 피하기 매우 쉬운 문제다. 단일 소유권을 강제하는 것(C++의 unique_ptr, 혹은 Rust의 더 포괄적인 borrow checker)은 훨씬 만들기도 어렵고 사용하기도 어렵다.
수동 메모리 회수(free, delete) 역시 버그나 충돌의 형태로 시간적 메모리 비안전성의 원천이 된다. 가비지 컬렉터는 성능과 메모리 사용 측면에서 어느 정도 대가를 치르는 대신, 이 안전성 문제 부류를 완전히 제거한다. borrow checking + lifetimes 또는 선형 타입(linear types)은 프로그래머가 메모리를 수동으로 해제하지 않도록 해 주는 또 다른 방법이다. 이 방식은 GC 관리 메모리보다 더 빠른 프로그램을 낳을 수 있지만, 프로그래머가 사용하기 더 어렵고 언어에 내장하기도 더 어렵고 컴파일 시간을 늘릴 수 있다.
멀티코어 프로세서가 표준이 되어 가면서 현대 언어는 이를 합리적으로 안전한 방식으로 사용할 수 있도록 지원해야 한다. 새로운 시스템 언어 대부분은 좋은 지원을 제공한다(C와 비교하면 어쨌든 그렇다). 앞에서 시간적 안전성과 공간적 안전성을 언급했다. 동시성은 시간적 안전성이 정말 큰 이점을 주는 영역이다. 스레드는 시간에 따라 실행되며, 어떤 스레드가 데이터를 사용하는 동안 그 데이터가 무효가 되지 않도록 보장하는 방법 등이 필요하다. Rust는 특히 이 점에서 뛰어나다. 내가 읽어 본 바로는(직접 써 보지는 않았다) Vale도 꽤 뛰어날 것 같다.
새로운 시스템 언어들은 대체로 순수한 힘보다 개발자 편의성을 우선한다. 학문적 언어 연구에서 나온 발전도 기존 관행에서 너무 멀어지지 않고 안전성을 더해 주는 경우에만 취한다. 더 나은 컴파일러와 도구를 위해 소프트웨어 공학의 발전도 받아들인다. Rust는 어느 정도 예외지만, 그렇다 해도 Haskell과 비교하면 Rust는 그렇게까지 배우기 어려운 언어는 아니다. Rust는 사용 편의성의 일부를 많은 안전성과 맞바꾼다.
다음 글에서는 새로운 시스템 언어들의 구체적인 차이점과 주목할 만한 특징을 많이 나열하고, 몇 가지 의견도 덧붙일 것이다.