Rust의 데이터 경쟁 방지 사례를 통해, 프로그래밍 언어 설계가 지역적 추론만으로 놀라운 전역 속성을 보장할 수 있는지 살펴본다.
지난 몇 년 동안, 나는 점점 더 자주 결국 이런 질문으로 귀결되는 이야기를 들었다. AI는 새로운 종류의 프로그래밍 언어로부터 이득을 볼까? 내 대답은 “아마 아닐 것”이었고, 적어도 지금까지는 그 대답이 꽤 잘 들어맞았다. AI는 이제 당신이나 내가 떠올릴 수 있는 거의 모든 프로그래밍 언어로 대량의 코드를 생성할 수 있다.
이제 기술이 발전했고, 그 특성이 더 분명해지기 시작하면서, 내 대답은 바뀌었다. 내 경험으로는 AI는 — 적어도 지금 이 시점에서는 — 고품질의 지역적 코드 조각(예를 들어 함수)은 자주 잘 만들어내지만, 프로그램 전체에 대한 전역적 이해가 필요한 코드를 생성하라는 요청을 받으면 종종 어려움을 겪는다. 이를 가장 쉽게 볼 수 있는 방식은 불필요한 방어적 검사의 확산이다. 이런 검사는 무해해 보이지만, 이후 코드를 읽는 사람들이 발생 가능하다고 믿게 되는 상태의 수를 기하급수적으로 늘릴 수 있으며, 그로 인해 온갖 해로운 효과가 뒤따른다.
어쩌면 이런 어려움은 곧 극복될지도 모르지만, 그렇지 않다면 우리는 다시 한 번 도움을 얻기 위해 프로그래밍 언어 설계로 눈을 돌릴 수 있다. 이 글의 목표는 프로그래밍 언어가 이 문제를 해결하려고 어떤 구체적인 방식을 시도할지, 혹은 시도해야 할지를 예측하려는 것이 아니다1. 대신 나는 더 기본적인 질문에 답하고 싶다. 지역적 추론만으로도 놀라운 전역 속성에 대한 확신을 줄 수 있게 하는 프로그래밍 언어 설계의 좋은 사례가 우리에게 있는가?
나는 생계의 상당 부분을 프로그래밍 언어로 벌어 왔기 때문에, 그 중요성을 강조할 이해관계가 있다. 하지만 프로그래밍 언어가 우리의 생산성과 우리가 만드는 소프트웨어의 신뢰성에 어느 정도 영향을 준다고 믿기는 해도, 그것이 근본적인 차이를 만든다는 증거는 많지 않다.
내가 말하는 것은 단지 “차이가 있다는 것을 증명하는 좋은 실험을 아무도 하지 못했다”는 뜻만은 아니다 — 물론 그것도 사실이지만! 오히려 “나쁜” 언어로도 많은 “좋은” 소프트웨어가 만들어졌고, “좋은” 언어로도 많은 “나쁜” 소프트웨어가 만들어졌다. 이런 결과의 주된 영향 요인이 사용된 특정 프로그래밍 언어였을 가능성은 낮아 보인다.
이에 대한 가장 단순한 논거는, 사용자가 필요로 하는 모든 일을 이해하기 쉽고 신뢰할 수 있는 방식으로 수행하는 소프트웨어를 만드는 데에는, 까다로운 프로그래밍 언어 기능에 대한 전문성보다 공감 능력이 더 필요하다는 점이다. 조금 더 미묘한 관점에 대해서는 예전에 소프트웨어의 본질에 대한 내 생각을 정리해 본 적이 있다.
그렇다고 해서 내가 프로그래밍 언어가 아무 차이도 만들지 않는다고 말하는 것으로 받아들여서는 안 된다. 내가 어셈블리에서 Python이나 C 같은 “고급” 언어로 옮겨 갔을 때, 생산성은 크게 높아졌고 훨씬 더 큰 규모의 소프트웨어에 도전할 수 있다고 느꼈다. 이유는 단순하다. 어셈블리는 너무 많은 저수준 세부사항을 다루게 강요해서, 나는 계속해서 더 중요한 고수준의 큰 그림을 잊어버리곤 했다. 내가 만들 수 있는 소프트웨어의 차이는 엄청났다.
안타깝게도, 나는 그런 정도의 거대한 향상이 다시 반복되기는 어렵다는 사실을 점점 깨닫게 되었다. 나는 천천히, 그리고 서투르게 Fred Brooks의 은탄환은 없다 논지를 재발명한 셈이었다.
과거 소프트웨어 생산성의 큰 향상 대부분은, 심각한 하드웨어 제약, 불편한 프로그래밍 언어, 부족한 머신 시간처럼 우발적 작업을 지나치게 어렵게 만들었던 인위적 장벽을 제거함으로써 이루어졌다. 오늘날 소프트웨어 엔지니어가 하는 일 중 얼마나 많은 부분이 본질적 작업이 아니라 우발적 작업에 여전히 쓰이고 있는가? 그것이 전체 노력의 9/10보다 크지 않다면, 모든 우발적 활동을 0 시간으로 줄인다고 해도 한 자릿수 크기의 도약적 향상은 얻을 수 없다.
그 말은, 훨씬 나중의 특정한 맥락에서 내가 쓰는 많은 소프트웨어에 대해 생산성이 다시 한 번 깊게 바뀌는 경험을 했을 때, 너무 놀라서 거의 눈치채지 못했다는 뜻이기도 하다. 결국 그 변화를 알아차리고 다른 사람들에게 그 차이를 설명하려고 했을 때, 그들 역시 어리둥절해했다. 그 맥락은 무엇이었을까? Rust에서의 멀티스레드 프로그래밍이다. 그 경험이 미래의 프로그래밍 언어가 나아갈 최선의 방향에 대한 내 의견을 형성했기 때문에, Rust가 멀티스레드 프로그래밍을 훨씬 쉽게 만드는 방식에 뭔가 깊은 점이 있다는 것을 당신에게 납득시켜야 한다.
구체적인 예로 시작해 보자. 나는 지금 당신이 읽고 있는 이 웹사이트를 빌드하는 소프트웨어를 평범한 단일 스레드 코드로 작성했다. 나는 게으르고 — 내 웹사이트가 그렇게까지 크지는 않기 때문에 — 실행할 때마다 웹사이트 전체를 다시 빌드한다.
시간이 지나면서, 사이트를 다시 빌드하는 동안 소프트웨어가 멈추는 시간이 길어져서 일부 페이지(예를 들어 이 글!)를 편집하는 일이 비효율적이 되었다. 나는 빠르게 몇 가지 단일 스레드 최적화를 했지만 충분하지 않았다. 그래서 이것을 멀티스레드로 다시 작성할 수 있다면 그 멈춤 시간을 수용 가능한 수준으로 줄일 수 있을 것이라고 추측했다.
거의 모든 다른 프로그래밍 언어에서라면, 이 소프트웨어를 멀티스레드를 사용하도록 다시 쓰는 일은 벅찬 작업이었을 것이다. 실제로 과거의 멀티스레딩 경험은, 내가 즉시 디버깅하기 어려운 크래시를 만나리라는 것을 보여주었다. 그리고 거의 확실히, 몇 주에서 몇 달에 걸쳐 하나씩 밟게 되는 그런 공포의 긴 꼬리가 뒤따랐을 것이다. 내가 멀티스레드 프로그램 작성을 그만둔 데에는 충분한 이유가 있다!
하지만 이 특정한 경우에는, 다시 쓰는 작업은 — 실제로 성능 문제를 해결했는데도 — 5분도 채 걸리지 않았다. 첫 시도에서 올바르게 동작했고, 그 이후로도 계속 올바르게 동작해 왔다 — 그리고 나는 이 두 가지가 모두 사실일 것이라는 완전한 확신을 가지고 있었다.
어떻게 이런 일이 가능할까? 나는 Rust를 무척 좋아한다 — 2015년 이후로 내 주력 언어였다 — 하지만 완벽한 언어는 아니다. 실제로 나는 그 결함을 자세히 늘어놓으며 사람들을 지루하게 만들 수 있고, 실제로 그렇게 해 왔다. 하지만 멀티스레딩에 관한 한, Rust는 내가 가능하리라고는 상상도 못했던 일을 해낸다. 데이터 경쟁 (즉, 조정되지 않은 읽기/쓰기 때문에 두 스레드가 서로에게 예기치 않게 간섭할 수 있는 상황)을 정적 오류로 만들어 버리는 것이다. 이것은 결코 작은 일이 아니다. 예전에는 멀티스레드 프로그램을 작성하려 할 때 데이터 경쟁이 단연 가장 큰 오류 원인이었기 때문이다2.
Rust3는 소유권 타입과 Send, Sync 트레이트의 조합을 통해 데이터 경쟁을 방지한다. Rust가 어떻게 동작하는지 안다면 이 절은 건너뛰어도 된다. Rust를 모른다면, 가능한 한 단순화하면서 이 기능들을 내가 할 수 있는 한 가장 짧게 개괄해 보겠다.
소유권 타입은 파고들다 보면 끝이 없지만, 여기서 우리가 알아야 할 것은 다음뿐이다. 주어진 객체에는 그것을 읽고 쓸 수 있는 소유자가 있고, 객체는 다른 소유자에게 이동될 수 있으며, 그 시점에서 이전 소유자는 그 객체에 대한 접근권을 잃고 다른 소유자가 접근권을 얻는다.
Send는 “이 struct의 인스턴스를 현재 스레드에서 다른 스레드로 이동시킬 수 있다”는 뜻이다 (즉 이동 후에는 현재 스레드가 그 객체에 접근할 수 없다). Sync는 “여러 스레드가 이 struct의 인스턴스를 동시에 읽을 수 있다”는 뜻이다. 여기서는 Rust가 어떤 struct가 Send 및/또는 Sync여도 안전한지 자동으로 판별하고, 그 트레이트를 자동으로 구현해 준다고 가정해도 된다.
아주 단순한 Rust 코드부터 시작해 보자.
fn main() { let x = vec![1, 2]; println!("{x:?}"); }
vec!가 만든 벡터는 Vec 타입의 인스턴스를 생성하며, 이 타입은 Send를 구현한다. 따라서 우리는 벡터를 다른 스레드로 보내고, 그 스레드가 벡터를 출력하게 할 수 있다.
fn main() { let x = vec![1, 2]; std::thread::spawn(move || println!("{x:?}")).join().ok(); }
std::thread::spawn(...)은 Rust에서 새 스레드를 만드는 방법이다. move || ...는 새 스레드가 시작될 때 실행할 “클로저”(즉 익명 함수)다. move는 바깥 함수에서 참조하는 모든 데이터의 소유자가 새 스레드가 됨을 의미한다 (즉 x는 새 스레드로 이동된다). join은 메인 스레드가 새 스레드가 끝날 때까지 기다린다는 뜻이다.
메인 스레드가 정말로 벡터에 대한 접근권을 잃었다는 것은 다음 코드가 보여준다.
fn main() { let x = vec![1, 2]; std::thread::spawn(move || println!("{x:?}")).join().ok(); println!("{x:?}"); } 이 코드는 다음과 같은 컴파일 타임 오류를 낳는다.
error[E0382]: borrow of moved value: x --> t.rs:4:14 | 2 | let x = vec![1, 2]; | - move occurs because x has type Vec<i32>, which does not implement the Copy trait 3 | std::thread::spawn(move || println!("{x:?}")).join().ok(); | ------- - variable moved due to use in closure | | | value moved into closure here 4 | println!("{x:?}"); | ^ value borrowed here after move | help: consider cloning the value before moving it into the closure | 3 ~ let value = x.clone(); 4 ~ std::thread::spawn(move || println!("{value:?}")).join().ok(); | error: aborting due to 1 previous error For more information about this error, try rustc --explain E0382.
나는 아직 본격적인 데이터 경쟁을 도입하기도 전에, Rust가 이미 내가 못된 짓을 하는 것을 막아 버렸다!
오류 메시지는 값을 clone하라고 제안한다. 숙련된 Rust 프로그래머는 이 조언에 조심스러운 편인데, 끔찍한 성능 저하로 이어질 수 있기 때문이다. 대신 객체를 참조 카운팅 타입 Rc로 감싸 보면 어떨까? 그러면 두 스레드에서 값을 즐겁게 공유할 수 있을 것 같다.
fn main() { let x = std::rc::Rc::new(vec![1, 2]); std::thread::spawn(move || println!("{x:?}")).join().ok(); println!("{x:?}"); } 하지만 안타깝게도 이는 다음과 같은 오류를 낳는다.
Rc<Vec<i32>> cannot be sent between threads safely
Rc 인스턴스를 다른 스레드로 이동시킬 수 없는 이유는 참조 카운팅이 스레드 안전한 방식으로 이루어지지 않기 때문이다. 다행히 이를 위한 변형이 있다. 바로 “원자적 참조 카운팅”인 Arc다. 조금 지루한 이유로, 나는 Arc를 복제해야 한다 (다행히 이 과정이 내부의 벡터까지 복제하지는 않는다!).
fn main() { let x = std::sync::Arc::new(vec![1, 2]); let y = std::sync::Arc::clone(&x); std::thread::spawn(move || println!("{y:?}")).join().ok(); println!("{x:?}"); }
이 코드는 컴파일되고 성공적으로 실행된다. 두 스레드는 같은 벡터를 읽고 같은 내용을 출력한다. 마지막으로 Rust의 표준 RefCell 타입을 도입해, 그 스레드들 사이에서 공유된 변경이 가능하도록 해 보자.
let x = std::sync::Arc::new(std::cell::RefCell::new(vec![1, 2]));
다시 오류가 나오는데, 이번에는 전송(Send)이 아니라 공유(Sync)에 관한 것이다.
error[E0277]: RefCell<Vec<i32>> cannot be shared between threads safely ... = help: the trait Sync is not implemented for RefCell<Vec<i32>>
아마도 이번이 우리가 처음으로 진짜 완전한 데이터 경쟁을 도입하려 했던 순간이라고 할 수 있을 것이다. 그런데도 Rust가 다시 우리를 막아선다. 스레드 간 공유된 변경 가능성을 허용하고 싶다면, 나는 Mutex 같은 타입을 도입해야 한다.
fn main() { let x = std::sync::Arc::new(std::sync::Mutex::new(vec![1, 2])); let y = std::sync::Arc::clone(&x); std::thread::spawn(move || { y.lock().unwrap().push(3); println!("{:?}", y.lock()); }).join().ok(); println!("{:?}", x.lock()); }
이 코드는 컴파일되고 올바르게 실행된다 (1, 2, 3을 두 번 출력한다).
이쯤에서 독자 여러분은, 바라건대, Rust가 내 프로그램에 데이터 경쟁을 도입하는 것을 막는다는 감각을 얻었을 것이다. 여기서 중요한 점 하나는, 이를 위해 Rust가 사실상 새로운 기능을 도입할 필요가 없었다는 것이다. 소유권 타입과 Send, Sync 트레이트만 있으면 충분하다. 다시 말해, 나는 여전히 “평범한” Rust 프로그램을 쓰고 있다. async 프로그램을 쓸 때처럼 새로운 서브언어를 사용할 필요가 없다.
Rust에서 멀티스레드 프로그램에 이점을 주는 규칙들은 숙련된 Rust 프로그래머에게 자연스럽고 당연하게 느껴지기 때문에, 우리는 더 깊은 진실을 놓치기 쉽다. Rust는 내 프로그램에 대해 전역적인 데이터 경쟁 없음 속성을 강제하면서도, 나는 그것을 지역적으로 추론할 수 있다. 예를 들어 이 속성은 함수 시그니처 수준에서 강제된다.
fn f<T: std::fmt::Debug>(x: T) { std::thread::spawn(move || println!("{x:?}")).join().ok(); } fn main() { f(vec![1,2]); }
내가 T에 제약을 걸지 않았기 때문에, Rust는 f의 호출자가 Send 가능한 객체를 f에 넘겼는지 확신할 수 없다. 그래서 2행의 spawn은 다음과 같은 오류를 낳는다.
T cannot be sent between threads safely
이것이 동작하려면, f는 자신에게 전달된 객체가 정말로 다른 스레드로 보내져도 되는 것임을 호출자에게 요구해야 한다. 문법은 다소 장황하다.
fn f<T: std::fmt::Debug>(x: T) where T: Send + 'static { std::thread::spawn(move || println!("{x:?}")).join().ok(); } fn main() { f(vec![1,2]); }
이제 이 코드는 컴파일되고 실행된다! 좋은 소식은, f의 시그니처를 보는 것만으로도 그것을 호출하는 일이 x에 대한 데이터 경쟁을 일으키지 않는다는 사실을 내가 확실히 알 수 있다는 점이다. 따라서 내가 Rc를 사용한 다음 조각은 컴파일되지 않는다.
f(std::rc::Rc::new(vec![1,2])); 하지만 이것을 다음처럼 바꾸면,
f(std::sync::Arc::new(vec![1,2])); 컴파일된다.
Rust에 익숙하지 않은 분들은 코드 조각이 여기서 끝난다는 사실에 기뻐할 것이다. 내가 이렇게 많은 예를 보여준 이유는, 이제 여러분이 다음 문장을 믿게 되기를 바라기 때문이다.
소유권 타입, Send, Sync의 조합은, 내가 코드의 지역적인 부분만 보면서도 멀티스레딩과 데이터 경쟁의 효과를 전역적으로 추론할 수 있게 해 준다.
이것은 평범한 정적 타입 보장처럼 들릴 수도 있다. 결국 내가 예를 들어 Haskell 프로그램을 쓴다면, 런타임에 타입 오류가 나지 않는다는 보장을 받는다. 그것은 사실이지만, Haskell의 일반적인 타입 시스템만으로는 동시성 코드가 데이터 경쟁으로부터 자유롭다는 Rust의 보장을 내게 주지 못한다.
다르게 말하면, Rust 이전까지 나는 표준 프로그래밍 언어가 과도한 고통 없이 강제할 수 있는 유일한 전역 속성은 “런타임 타입 오류가 없다”는 것뿐이라고 암묵적으로 가정하고 있었다4. 나는 그런 속성을 얻으려면 이국적이거나 실험적인 언어를 써야 하고, 그 과정에서 필요한 타협은 소수의 프로그래머만 받아들일 수 있으리라고 생각했다. Rust의 데이터 경쟁 자유 보장은 정확하고, 규칙이 깨질 때의 오류도 (대체로) 이해 가능하며, 전체적인 결과는 매우 실용적이다5.
이제 우리는 원래 주제로 돌아갈 수 있다. 어느 정도 규모만 되어도 프로그램을 어렵게 만드는 이유는, 각각의 지역적 변화가 하나의 나비이기 때문이다 — 그리고 그 나비들의 날갯짓 중 일부는 멀리 떨어진 곳에 큰 폭풍(즉 버그!)을 일으킨다.
이것은 언제나 문제였다. 가장 뛰어난 인간 프로그래머조차도 자신이 작업하는 소프트웨어에 대한 전역적 관점을 획득하고 유지하는 데 어려움을 겪는다. 그런데 지금은 AI가 종종 그보다 더 큰 어려움을 겪는다.
명확히 정의된 명세를 가진 단일 함수를 생성하라고 AI에게 요청하면, 종종 나보다 더 나은 코드를 더 빠르게 만들어 낸다. 하지만 어느 정도 크기의 소프트웨어를 생성한 다음 그것을 다듬으라고 하면, 결과는 종종 매력적이지 않다. 사람들은 이와 관련해 코드 비대화를 자주 말하는데, 그것도 사실이지만 더 깊은 문제를 놓친다. 시스템에 대한 전역적 관점이 생성된 코드에 서툴게, 때로는 잘못되게 담겨 있는 경우가 많다는 점이다. 이 문제를 가장 쉽게 — 물론 유일한 방식은 결코 아니지만! — 알아볼 수 있는 방법은, AI가 생성한 코드가 엄청난 수의 방어적 검사 를 포함하는 경향이 있다는 점이다.
assertion과 방어적 검사는 때때로 혼동되지만, 둘은 매우 다르다. assertion은 예상치 못한 상황이 관찰되는 즉시 프로그램을 중단시킨다. 그것은 “이 조건이 성립하지 않는다면, 프로그래머가 시스템의 동작 방식을 오해했거나 시스템의 다른 부분이 잘못되었다”는 생각을 담는다. 반대로 방어적 검사는 프로그램을 중단시키지 않는다. 검사가 실패해도 실행은 의도적으로 계속된다. 따라서 방어적 검사는 “이 조건이 성립하는지 확신할 수는 없지만, 실패하더라도 그것을 우아하게 처리할 방법을 갖고 싶다”는 생각을 담는 것으로 보는 편이 더 적절하다.
방어적 검사는 명백하게 좋은 것처럼 보인다. 현재 작업은 일찍 끝나는 경향이 있지만, 프로그램 전체는 계속 실행된다. 하지만 좋은 것도 지나치면 문제가 된다. 사람이 쓴 코드와 마찬가지로, AI가 생성한 코드의 많은 방어적 검사는 불필요하다 (즉 실패할 수 없다).
내가 많은 AI 생성 코드에서 자주 보는 흔한 예는 “이 리스트는 비어 있지 않아야 한다”는 검사인데, 실제로는 그 검사에 이르는 모든 경로에서 이미 그것이 확인되었고, 종종 여러 번 확인되었기까지 하다. 다음과 같은 코드는 흔하다.
def f(x): if len(x) == 0: return else: ... # do something with x # This is the only place that f is called from for x in ...: if len(x) > 0: f(x)
이 예에서 f 안의 방어적 검사는 잘해야 불필요할 뿐이다67.
불필요한 검사는 거슬리고, 성능을 해치며, 특정 시점에서 프로그램의 상태가 어떠한지에 대해 코드를 읽는 사람을 오도한다. 우리는 이 마지막 문제의 해악이 얼마나 큰지 자주 과소평가한다. 당신이 어떤 수정 지점에서 프로그램이 상태 A, B, C에 있을 수 있다고 생각한다면, 세 상태를 모두 고려해야 한다. 그런데 실제로는 그중 두 상태가 발생할 수 없다면, 당신은 단지 노력을 낭비했을 뿐 아니라, 이후 수정에서 고려해야 할 상태 수를 잠재적으로 기하급수적으로 폭발시키는 결과를 만든 것이다. 그리고 그에 따라 생산성과 신뢰성에 대한 모든 악영향이 뒤따른다.
지난 1년 동안 AI 코드 생성이 얼마나 빠르게 발전했는지를 생각하면, 이 문제가 곧 해결될 가능성을 배제하는 것은 어리석을 것이다. 하지만 또 다른 중대한 돌파구 한두 개가 없는 한, AI가 계속해서 지역적으로는 탁월하고 전역적으로는 약할 가능성도 있다. 만약 그렇다면8, 우리는 과거보다 훨씬 더 강한 동기를 갖고 프로그래밍 언어가 전역 속성을 강제하는 일을 도와주기를 바라게 될 것이다. 그렇게 할 때마다 우리는 버그의 한 전체 부류를 제거하기 때문이다.
이 글에서 내가 보여주고 싶었던 것은, 놀라운 전역 속성 — 데이터 경쟁 없음 — 이 순전히 지역적 추론을 통해 강제될 수 있다는 점이다. 내가 보기에 여기서 흥미로운 점은, 이것이 프로그래밍의 중요한 여러 작업을 더 신뢰할 수 있게 만들면서도 프로그래머에게 거의 추가적인 부담을 지우지 않는다는 것이다. 이것은 다른 바람직한 전역 속성들도 프로그래밍 언어 설계를 통해 비슷하게 다룰 수 있으리라는 희망을 준다9.
물론 우리는 현실적이어야 한다. 우리가 원하는 모든 전역 속성을 다 강제할 수는 없을 것이다. 그 속성들은 성능 보장, 하위 구성요소의 격리, 여러 종류의 비간섭성, 자원 정리, 상태 변화 등에 대한 보장에 이르기까지 다양할 수 있다. 그중 일부는 거의 확실히 서로 충돌할 것이고, 일부는 그만한 가치가 없을 정도로 부담스러울 것이며, 일부는 아예 다룰 수 없을 것이다.
좋은 소식은, 이와 관련이 있을지도 모르는 실험적 프로그래밍 언어 기능들이 여러 가지 존재한다는 점이다10. 예를 들어 효과 시스템은 프로그램의 어떤 부분이 입출력을 수행하는지를 지역적 추론을 통해 이해할 수 있게 해 준다. 현재로서는, 그 어떤 것도 내가 이 글에서 이야기한 Rust 기능들과 같은 규모로 시험된 적은 없다. 어떤 것이, 있다면 무엇이, 승자가 될지는 알기 어렵다. 아직 생각조차 되지 않은 기능이 무엇일지도 알 수 없다.
그럼에도 불구하고, 미래의 실용적인 프로그래밍 언어가 전역 속성에 대해 훨씬 더 많은 보장을 가진 프로그램을 쓰게 해 줄 것이라고 현실적으로 상상할 만큼의 증거는 충분하다. 프로그램을 만들고 수정하는 주체가 누구이든, 혹은 무엇이든, 그것으로부터 상당한 이득을 얻을 수 있을 것이다. 개인적으로 나는 그것이 우리로 하여금 프로그램을 구조화하는 방식을 완전히 다시 생각하게 해 주기를 바란다!
요약하자면, 최근까지 나는 AI가 프로그래밍 언어에 대한 우리의 유인을 바꾼다고 생각하지 않았다. 이제 그 유인은 변하고 있으며, 다행히도 우리는 그 유인에 대응할 수 있을지도 모른다는 몇 가지 징후를 갖고 있다. 충분한 자원을 가진 누군가가 이런 수준의 야망을 가지고 있는지는 나로서는 알 수 없다. 하지만 적어도 하나의 큰 회사가 — 그리고 2026년에도 이것은 거의 확실히 회사의 자원을 필요로 할 것이다 — 그렇게 하려 시도하더라도 놀랍지 않을 것이다. 나는 미리부터 그들을 응원한다. 앞으로 매우 흥미로운 프로그래밍 언어들이 나올지도 모르기 때문이다!
2026-06-30 10:50 이전 글
새 블로그 글 업데이트를 받고 싶다면: Mastodon이나 Twitter에서 나를 팔로우하거나, RSS 피드 구독을 하거나, 이메일 업데이트 구독을 할 수 있다:
[1](https://tratt.net/laurie/blog/2026/local_reasoning_for_global_properties.html)
부분적으로는 이것이 프로그래밍 언어 연구/설계에서 활발한 분야이기 때문이고, 부분적으로는 AI 코드 생성이 어떻게 변할지를 추측하는 일이 내 전문 범위를 넘어가기 때문이다.
☒
부분적으로는 이것이 프로그래밍 언어 연구/설계에서 활발한 분야이기 때문이고, 부분적으로는 AI 코드 생성이 어떻게 변할지를 추측하는 일이 내 전문 범위를 넘어가기 때문이다.
[2](https://tratt.net/laurie/blog/2026/local_reasoning_for_global_properties.html)
물론 마주칠 수 있는 다른 종류의 오류들도 있다. 특히 데드락이 그렇다. 그래도 그런 오류는 디버깅하기가 더 쉽고, 내 경험상 훨씬 덜 자주 발생한다.
☒
물론 마주칠 수 있는 다른 종류의 오류들도 있다. 특히 데드락이 그렇다. 그래도 그런 오류는 디버깅하기가 더 쉽고, 내 경험상 훨씬 덜 자주 발생한다.
[3](https://tratt.net/laurie/blog/2026/local_reasoning_for_global_properties.html)
엄밀히 말하면 “safe Rust”다.
☒
엄밀히 말하면 “safe Rust”다.
[4](https://tratt.net/laurie/blog/2026/local_reasoning_for_global_properties.html)
Pony 같은 다른 언어들도 물론 같은 보장을 제공하지만, 메커니즘은 꽤 다르다.
☒
Pony 같은 다른 언어들도 물론 같은 보장을 제공하지만, 메커니즘은 꽤 다르다.
[5](https://tratt.net/laurie/blog/2026/local_reasoning_for_global_properties.html)
약간의 예외를 들자면, Arc::clone을 너무 자주 호출하게 되어 눈에 거슬리게 되는 경향은 있다.
☒
약간의 예외를 들자면, Arc::clone을 너무 자주 호출하게 되어 눈에 거슬리게 되는 경향은 있다.
[6](https://tratt.net/laurie/blog/2026/local_reasoning_for_global_properties.html)
f가 무엇을 하기로 되어 있는지에 따라, 그 검사는 심지어 잘못되었을 수도 있다. 나는 전달된 원소들의 합을 출력해야 하는 print_sum 같은 이름의 함수가, 빈 리스트에 대해서는 아무것도 출력하지 않는 경우를 본 적이 있다!
☒
f가 무엇을 하기로 되어 있는지에 따라, 그 검사는 심지어 잘못되었을 수도 있다. 나는 전달된 원소들의 합을 출력해야 하는 print_sum 같은 이름의 함수가, 빈 리스트에 대해서는 아무것도 출력하지 않는 경우를 본 적이 있다!
[7](https://tratt.net/laurie/blog/2026/local_reasoning_for_global_properties.html)
용감한 프로그래머라면 그 검사를 완전히 제거할 수도 있겠지만, 나는 assertion으로 표현하는 쪽을 택할 것이다.
☒
용감한 프로그래머라면 그 검사를 완전히 제거할 수도 있겠지만, 나는 assertion으로 표현하는 쪽을 택할 것이다.
[8](https://tratt.net/laurie/blog/2026/local_reasoning_for_global_properties.html)
어쩌면 그렇지 않더라도 마찬가지일 수 있다. 인간이 오로지 AI가 생성한 코드를 검토하는 역할로 밀려난다 해도, 우리가 읽고 있는 코드에 대해 더 쉽게 확신을 가질 수 있게 해 주는 모든 것은 유용할 것이다.
☒
어쩌면 그렇지 않더라도 마찬가지일 수 있다. 인간이 오로지 AI가 생성한 코드를 검토하는 역할로 밀려난다 해도, 우리가 읽고 있는 코드에 대해 더 쉽게 확신을 가질 수 있게 해 주는 모든 것은 유용할 것이다.
[9](https://tratt.net/laurie/blog/2026/local_reasoning_for_global_properties.html)
나 역시 이런 언어 설계가 어떤 모습일지, 어떤 모습이 아닐지, 혹은 Rust의 영향을 받을지조차 전혀 알지 못한다. 이 글에서 내가 말하고자 하는 전체 요점은, 내가 동기 부여 사례로 사용한 특정 언어와는 독립적이라고 믿는다.
☒
나 역시 이런 언어 설계가 어떤 모습일지, 어떤 모습이 아닐지, 혹은 Rust의 영향을 받을지조차 전혀 알지 못한다. 이 글에서 내가 말하고자 하는 전체 요점은, 내가 동기 부여 사례로 사용한 특정 언어와는 독립적이라고 믿는다.
[10](https://tratt.net/laurie/blog/2026/local_reasoning_for_global_properties.html)
프로그래밍에 형식 기법이 외부적으로 점점 더 통합되고 있다는 점은 말할 것도 없다.
☒
프로그래밍에 형식 기법이 외부적으로 점점 더 통합되고 있다는 점은 말할 것도 없다.