프로그래밍의 세계 1강. 기계어에서 사람의 말로
컴퓨터는 지금도 0과 1만 먹습니다. 지난 80년의 언어사는 사람이 기계 쪽으로 내려가던 길을 기계가 사람 쪽으로 올라오게 뒤집은 역사입니다.
풀고 시작
돌아간 적 없는 최초의 프로그램
1843년, 에이다 러브레이스는 찰스 배비지의 해석기관을 소개한 이탈리아 수학자 메나브레아의 글을 영어로 옮기면서 원문보다 긴 주석을 붙였습니다. 그 마지막 주석인 노트 G에는 베르누이 수를 기계로 계산하는 절차가 표 형태로 실려 있습니다. 해석기관은 끝내 완성되지 않았으니 이 절차는 한 번도 실행된 적이 없습니다. 그런데도 이 노트가 계속 소환되는 이유는 따로 있습니다. 러브레이스는 이 기계를 숫자 계산기가 아니라 기호를 다루는 장치로 보았고, 규칙으로 표현할 수 있는 대상이라면 숫자가 아니어도 다룰 수 있다고 적었습니다. 하드웨어와 절차를 분리해서 생각한 것이죠. 다만 "최초의 프로그래머"라는 호칭에는 논쟁이 있습니다. 배비지가 앞서 비슷한 절차표를 작성해 두었다는 자료가 있고, 노트의 기여를 어떻게 나눌지에 대해 연구자들의 평가가 갈립니다.
0과 1 위에 이름표를 붙이다
프로세서는 명령을 이진 비트열로 받습니다. 앞쪽 비트는 무엇을 할지 지시하는 연산 코드(opcode)이고 뒤쪽 비트는 대상 레지스터나 메모리 주소입니다. 초기 프로그래머들은 이 비트열을 직접 적었고, 숫자 하나가 어긋나면 기계는 엉뚱한 주소로 뛰었습니다. 어셈블리 언어는 여기에 최소한의 구원을 줍니다. 각 명령에 ADD나 MOV 같은 니모닉 이름을 붙이고 주소에도 이름표를 달아 두는 것이죠. 그러면 어셈블러라는 프로그램이 그 이름을 정해진 비트 패턴으로 되돌립니다. 비유하자면 사전을 옆에 둔 것이지 통역사를 고용한 것은 아닙니다. 실제 벌어지는 일이 대체로 일대일 치환이기 때문에, 프로세서 계열이 달라지면 코드를 처음부터 다시 써야 합니다. 이식성이라는 문제가 이렇게 태어났습니다.
1950년대 말, 세 갈래 길
1957년 IBM의 존 배커스 팀이 포트란(FORTRAN)을 내놓습니다. 수식을 수식처럼 적으면 기계어로 번역해 주는 물건이었죠. 당시 반응은 환영이 아니라 의심이었습니다. 손으로 다듬은 기계어보다 기계가 뽑은 코드가 느릴 것이라는 통념이 강했고, 그래서 배커스 팀은 번역기의 최적화에 집착했습니다. 1959년에 착수된 코볼(COBOL)은 정반대 방향을 겨냥합니다. 사무 처리 담당자가 읽을 수 있도록 영어 문장에 가까운 문법을 택했고, 그레이스 호퍼가 만든 FLOW-MATIC이 그 바탕이 되었습니다. 1958년 존 매카시의 리스프(LISP)는 또 다른 세계를 엽니다. 프로그램 자체가 리스트라는 데이터 구조여서 프로그램이 프로그램을 만들어 낼 수 있었고, 이 성질이 기호 처리와 초기 인공지능 연구의 도구가 되었습니다. 수치 계산, 업무 데이터, 기호 조작이라는 세 갈래는 지금 쓰는 언어들의 족보에 그대로 남아 있습니다.
C가 아직도 바닥에 있는 이유
1970년대 초 벨 연구소의 데니스 리치가 C를 만들고, 켄 톰프슨과 함께 어셈블리로 짜여 있던 유닉스를 C로 다시 씁니다. 이 사건이 결정적이었습니다. 운영체제조차 다른 기계로 옮길 수 있게 되었기 때문입니다. C는 함수와 제어 구조라는 고수준 문법을 주면서도 메모리 주소를 직접 다루도록 허용해서 "이식 가능한 어셈블리"라는 별명을 얻었고, 지금도 커널과 임베디드 기기와 다른 언어들의 런타임이 C로 쓰여 있습니다. 대가도 분명합니다. C는 배열의 범위나 이미 해제된 메모리를 검사하지 않기 때문에, 프로그램이 남의 메모리를 건드리는 결함이 조용히 살아남습니다. 그래서 메모리 안전을 언어 차원에서 강제하는 언어로 옮기자는 흐름이 최근 힘을 얻고 있는데, 이미 쌓인 C 코드의 규모 때문에 어디까지 얼마나 빨리 바꿀지는 아직 논쟁 중입니다.
번역할 것인가, 통역할 것인가
컴파일러는 소스 전체를 미리 읽어 기계어로 번역해 실행 파일을 남깁니다. 인터프리터는 실행하는 순간에 코드를 읽어 그에 해당하는 동작을 곧바로 수행하고 번역물을 남기지 않습니다. 다만 현실은 두 극단 사이의 스펙트럼입니다. 자바와 파이썬은 소스를 바이트코드로 컴파일한 뒤 가상 머신이 그것을 해석하고, JIT(just-in-time) 방식은 실행 도중 자주 도는 부분만 골라 기계어로 바꿔 둡니다. 그래서 "컴파일 언어"와 "인터프리터 언어"라는 구분은 언어의 성질이 아니라 구현의 선택입니다. 고수준 언어가 계속 늘어난 이유도 같은 자리에 있습니다. 하드웨어 값은 내려가고 사람의 시간과 실수는 비싸졌기 때문에, 기계에게 맡길 수 있는 일을 넘길수록 이득이 커졌습니다. 그 실수를 부르는 이름의 유래는 버그의 유래에 있습니다. 다음 강에서는 같은 문제를 서로 다르게 바라보는 패러다임들을 만납니다.
인출 문제
생각해볼 질문 (정답 없음)
- 언어가 사람 쪽으로 올라올수록 프로그래머는 아래에서 실제로 무슨 일이 벌어지는지 점점 모르게 됩니다. 이것은 발전일까요, 아니면 언젠가 대가를 치를 빚일까요.
이전: 알고리즘과 자료구조 5강 · 다음: 프로그래밍의 세계 2강