Proven C Book←↑→

9 스트림의 기원 — 펀치카드, 라인프린터, 프린터 터미널

먼저 알아야 할 것

8장 문자와 텍스트 · 글자도 결국 바이트의 약속이다

돌아보기

8장에서 제어 문자 — “종이를 한 줄 올려라” 같은, 장치에 내리는 지시 — 가 문자표의 한 구석에 산다고 했다. 글자들의 표에 왜 장치 조작 지시가 끼어 있는가?

답. 그 표가 만들어진 시대에는 텍스트가 곧 장치의 움직임이었기 때문이다. 글자는 화면에 그려지는 것이 아니라 종이에 물리적으로 찍히는 것이었고, “다음 줄로”는 실제로 기계 부품이 움직이는 동작이었다. 그 시대의 장치들을 구경하면, 오늘의 입출력이 왜 이런 모양인지 전부 풀린다.

이 장의 필요성과 맥락

표현을 배웠으니 이제 그것이 드나드는 통로다. 이 장이 16장 바로 앞 언저리에 있는 것은 우연이 아니다 — 첫 프로그램의 printf 가 어디에 대고 말하는지, \n 이라는 두 글자가 왜 그 모양인지가 여기서 풀린다. 종이의 시대를 먼저 보아야 스트림이 「C 의 설계」가 아니라 「물려받은 것」임을 안다.

이 장이 끝나면

컴퓨터의 입출력이 왜 “문자가 한 줄씩 흐르는 것”의 모습인지 알게 된다. 답은 종이의 시대에 있다 — 펀치카드, 라인프린터, 그리고 타자기를 닮은 프린터 터미널(terminal). \n이라는 두 글자와 tty라는 이상한 이름의 유래도 여기서 풀린다. 이 장이 끝나면 곧 만날 첫 프로그램의 printf가 어디에 대고 말하는 것인지 미리 이해하게 된다.

이 장에서 답할 질문

  1. 카드 뭉치를 떨어뜨리면 어떻게 되는가?
  2. 그러면 「비우기」는 들어오는 쪽에도 있는가? 입력 버퍼에 남은 것을 치우고 싶을 때가 있을 텐데.
  3. 요즘 프로그램은 창과 버튼으로 소통하는데, 스트림은 옛날이야기 아닌가?

9.1 펀치카드 — 한 장이 한 줄이다#

초기 컴퓨팅의 입력은 펀치카드였다. 뻣뻣한 종이 카드에 구멍을 뚫어 글자를 기록한다 — 구멍의 조합이 글자 하나이고, 카드 한 장에 글자가 80칸 들어간다. 프로그램 한 줄을 카드 한 장에 뚫고, 프로그램 전체는 카드 뭉치가 된다. 뭉치를 기계에 먹이면 카드가 한 장씩 빨려 들어가며 읽힌다.

여기서 두 가지 유산이 태어났다. 첫째, “한 장 = 한 줄”이라는 감각. 텍스트는 줄들의 열이라는, 지금은 공기처럼 당연한 관념이 물리적 실물에서 왔다. 둘째, 80이라는 수. 카드의 80칸은 이후 터미널 화면의 80컬럼으로, “코드 한 줄은 80자 안쪽으로”라는 오늘날의 코딩 관행으로 반세기를 건너 살아남았다.

문. 카드 뭉치를 떨어뜨리면 어떻게 되는가?

답. 대참사다 — 순서가 섞인 카드 수백 장이 곧 프로그램이 뒤섞인 것이다. 그래서 카드 구석에 일련번호를 뚫어 두고 기계로 다시 정렬하는 요령이 있었다. 카드 한 귀퉁이를 사선으로 잘라 뒤집힌 카드를 눈으로 찾게 한 것도 같은 지혜다. 우스개 같지만, “데이터에 순서 번호를 붙여 두면 섞여도 복구할 수 있다”는 착상은 지금도 통신과 데이터베이스 설계의 기본기다.

9.2 라인프린터 — 줄 단위로 찍는 출력#

출력 쪽의 실물은 라인프린터였다. 이름 그대로 한 번에 한 줄을 통째로 종이에 찍는 인쇄기다. 계산 결과는 화면이 아니라 연속 용지에 줄줄이 찍혀 나왔다. 입력은 카드 한 장씩(= 한 줄씩), 출력은 프린터 한 줄씩 — 컴퓨터의 입출력은 태생부터 줄 단위의 흐름이었다.

9.3 프린터 터미널 — 종이 위의 대화, 그리고 tty#

시분할의 시대가 오며(여러 사람이 한 컴퓨터를 동시에 쓰는 방식) 사람과 컴퓨터가 대화할 장치가 필요해졌다. 그 자리에 들어온 것이 전신 타자기 — 텔레타이프(Teletype)다. 타자기처럼 생긴 이 기계로 글쇠를 치면 그 글자가 컴퓨터로 가고, 컴퓨터의 응답이 같은 기계의 종이에 찍힌다. 화면은 없다. 대화 전체가 종이 두루마리에 인쇄된다.

Unix가 바로 이 장치들 곁에서 태어났고(2장), 단말 장치를 가리키는 이름을 텔레타이프의 약자에서 따 왔다 — tty. 오늘날의 터미널 창 밑바닥에 여전히 살아 있는 이름이다.

타자기의 몸에서 온 유산이 또 있다. 타자기에서 줄을 바꾸는 동작은 사실 두 개다 — 활자 뭉치(캐리지)를 왼쪽 끝으로 되돌리는 동작과, 종이를 한 줄 올리는 동작. 문자표는 이 두 동작에 각각 제어 문자를 배정했다. 캐리지 리턴(CR, C 표기 \r)과 라인 피드(LF, \n)다. 줄 하나를 바꾸는 데 지시 두 개가 필요했던 것은 기계 부품이 실제로 둘 움직였기 때문이다.

실제 사례. CRLF — 타자기가 남긴 백 년짜리 숙제

물리적 이유가 사라진 뒤에도 두 문자는 남았고, 세계는 줄바꿈 표기를 통일하지 못했다. Windows 계열은 두 동작을 다 적는 \r\n(CRLF)을, Unix 계열은 \n 하나를 줄의 끝으로 삼았다. 그 결과가 오늘날의 일상적 사고들이다 — 한쪽에서 만든 텍스트 파일을 다른 쪽에서 열면 줄 끝에 유령 문자가 붙거나 온 파일이 한 줄로 보이고, 버전 관리 도구(git)는 “CRLF를 LF로 바꿀 것”이라는 경고를 낸다. 타자기 부품의 움직임이, 부품이 사라진 지 반세기 뒤의 협업 현장에서 아직 경고문을 띄우고 있는 셈이다.

그리고 이 두 문자는 보안에도 얼굴을 내민다. 인터넷 규약들(HTTP — hypertext transfer protocol —, 전자우편)은 줄바꿈으로 항목을 나누는 텍스트 규약이라서 — 사용자가 준 값에 CR·LF가 섞여 들어가면 공격자가 새 항목을 만들어 끼워 넣을 수 있다. 응답 헤더에 남의 값을 그대로 넣었다가 헤더가 통째로 조작되거나(HTTP 응답 분할), 로그 한 줄에 남의 값을 넣었다가 가짜 로그 줄이 위조되는 식이다(로그 위조). 8장의 후렴구가 여기서도 반복된다 — 데이터를 틀에 섞지 말 것, 그리고 경계에서 제어 문자를 걸러 낼 것.

9.4 화면 터미널, 그리고 스트림이라는 유산#

이윽고 종이가 화면으로 바뀌었다 — 그러나 새 화면 장치들은 프린터 터미널을 흉내 내는 방식으로 만들어졌다. 글자가 아래에서 위로 밀려 올라가는 오늘의 터미널 창은, 종이가 위로 밀려 올라가던 텔레타이프의 유리판 버전이다. 겉모습만 바뀌고 대화의 문법은 그대로 남은 것이다.

이 물리적 계보 전체가 하나의 관념으로 응축됐다. 입출력이란 문자가 한 줄씩, 순서대로 흘러가는 띠라는 관념 — 이것이 스트림(stream)이다. 카드가 한 장씩 빨려 들어가듯 입력이 흘러 들어오고, 프린터가 한 줄씩 찍듯 출력이 흘러 나간다. C는 이 관념을 언어의 입출력 모델로 그대로 받아들였다. 프로그램마다 기본으로 주어지는 표준 입력과 표준 출력(standard output) 이라는 두 흐름이 그것이고, 이 책의 다음 부에서 만날 printf는 바로 표준 출력이라는 띠에 글자를 흘려보내는 함수다.

9.5 띠의 중간에는 통이 있다 — 버퍼#

여기서 그림을 하나 더 얹어 두자. 지금까지 띠는 프로그램과 장치를 곧장 잇는 것처럼 보였다. 실제로는 그 사이에 통이 하나씩 있다고 상상하는 편이 옳다 — 나가는 쪽에 출력 버퍼, 들어오는 쪽에 입력 버퍼.

왜 통을 두는가. 이유는 하나가 아니다.

통이 있다는 사실은 곧 글자가 흘러가는 시점과 프로그램이 쓴 시점이 다르다는 뜻이다. 그래서 「내보내라」고 명시적으로 말하는 동작이 따로 있다 — 비우기(flush)다.

문. 그러면 「비우기」는 들어오는 쪽에도 있는가? 입력 버퍼에 남은 것을 치우고 싶을 때가 있을 텐데.

답. 없다. 이것이 이 절에서 가장 자주 헛짚는 자리다. 비우기는 출력 쪽 동작이다 — 통에 고인 것을 밖으로 내보내는 일이니까. 들어오는 쪽의 통은 밖으로 내보낼 곳이 없다. 그 안의 글자는 누군가 읽어 가야 없어진다.

그래서 「입력 버퍼를 비운다」는 말은 실은 「남은 것을 읽어서 버린다」는 뜻이고, 그렇게 손으로 해야 한다. 표준 라이브러리의 비우기 함수에 입력 스트림을 넘기는 관행이 널리 퍼져 있지만 그것은 계약 밖이다 — 68장에서 정확한 이유와 함께 다시 본다.

다만 그보다 앞서 물어야 할 것이 있다. 무엇을 버리라는 말인가? 이 물음이 왜 무거운지는 바로 다음 문단에서 본다.

띠의 반대편에 무엇이 있는지 프로그램은 모른다 — 이 장의 앞에서 본 리다이렉션과 파이프가 바로 그 「모름」을 제도로 만든 것이다. 사람이 한 줄씩 치는 터미널일 수도 있고, 파일이 통째로 이어져 이미 다 와 있을 수도 있고, 앞 프로그램이 아직 돌고 있어 조금씩 오는 중일 수도 있고, 네트워크 건너에서 띄엄띄엄 건너올 수도 있다.

그러면 「지금 통을 비워라」라는 지시는 무슨 뜻이 되는가. 마침 그 순간까지 도착해 있던 만큼을 버리라는 뜻이다. 얼마나 버릴지는 내 프로그램의 논리가 아니라 상대의 속도가 정한다 — 같은 프로그램이 어제와 오늘 다르게 도는 자리이고, 터미널에서는 멀쩡하다가 파이프로 이으면 어긋나는 자리다.

그래서 버릴 것을 정할 때는 시점이 아니라 경계로 말해야 한다. 「지금까지 온 것」이 아니라 「개행까지」처럼, 프로그램이 스스로 말할 수 있는 표시로. 이것이 26장에서 서식 읽기를 다룰 때 되풀이될 이야기의 씨앗이다.

그리고 들어오는 쪽에는 나가는 쪽에 없는 물음이 하나 더 있다. 통에 글자가 쌓여 있을 때, 어디까지가 이번에 읽을 것이고 어디부터는 남겨 둘 것인가. 「정수 하나를 읽어라」는 말은 통 안의 42\n 에서 어디까지를 가져가라는 뜻인가 — 42 인가, 개행까지인가?

이 물음에는 정답이 없고 약속만 있다. 읽는 쪽이 「이런 모양이 올 것이다」라고 예상하고, 그 예상에 맞는 데까지만 가져간 뒤, 맞지 않는 첫 글자부터는 통에 그대로 남긴다 — 그것이 다음 읽기의 몫이 된다. 이 「예상하고, 가져가고, 남긴다」가 입력을 다루는 모든 함수의 뼈대이고, 초보자가 겪는 입력 사고의 대부분이 남은 것을 잊는 데서 온다. 26장이 이 이야기를 실제 프로그램으로 이어 간다.

9.6 띠를 갈아 끼우다 — 리다이렉션과 파이프라인#

스트림이라는 관념에는 뜻밖의 쓸모가 하나 숨어 있었다. 프로그램이 자기 출력의 끝이 어디인지 모른다면 — 그 끝을 바깥에서 갈아 끼울 수 있다는 뜻이다.

Unix가 이 착상을 제도로 만들었다. 프로그램은 표준 입력·표준 출력이라는 띠에 대고 읽고 쓸 뿐이고, 그 띠의 반대편을 무엇에 연결할지는 프로그램을 실행하는 쪽이 정한다. 터미널에서 명령 뒤에 > 파일.txt라고 적으면 출력의 띠가 화면 대신 파일로 이어지고(리다이렉션), < 입력.txt라고 적으면 입력의 띠가 키보드 대신 파일에서 흘러온다. 프로그램의 코드는 한 글자도 바뀌지 않는다.

여기서 한 걸음 더 나간 것이 파이프라인(pipeline)이다. 어떤 프로그램의 출력 띠를 다른 프로그램의 입력 띠에 곧장 잇는 것 — 터미널의 | 기호가 그 연결이다.

$ 목록내기 | 걸러내기 | 세기

세 프로그램이 각각 자기 띠만 보고 일하는데, 셋을 이어 붙이면 하나의 새 도구가 된다. 이 연결을 발명한 더글러스 매클로이(Doug McIlroy)와 Unix 사람들이 남긴 원칙이 이후 소프트웨어 설계의 격언이 됐다 — “한 가지 일을 잘하는 작은 프로그램을 만들고, 텍스트 스트림으로 협력하게 하라.” 프로그램들을 부품으로 쓰는 조립의 문법이, 겨우 이 “띠를 갈아 끼우는” 단순함에서 나온 것이다.

이 자유의 대가로 프로그래머가 지켜야 할 것도 생긴다. 내 프로그램의 출력이 언제나 사람의 화면으로 간다고 가정하면 안 된다는 것 — 장식용 글자나 진행 표시가 뒤 프로그램의 입력을 오염시킬 수 있다. 그래서 Unix 전통은 통로를 하나 더 둔다: 진짜 결과는 표준 출력으로, 사람에게 하는 말(경고·오류)은 표준 오류라는 별도의 띠로 보내는 것이다. 세 번째 띠의 존재 이유가 이것이다.

실제 사례. 이 책의 실행 결과도 리다이렉션의 산물이다

추상적인 이야기가 아니다 — 지금 읽고 있는 이 책이 그 증거다. 이 책의 모든 코드 시연은 기계가 예제를 실행해 얻은 진짜 출력인데, 그 출력을 사람이 받아 적은 것이 아니라 출력의 띠를 파일로 돌려 (리다이렉션) 조판 도구가 그 파일을 읽어 인쇄한 것이다. 입력이 필요한 예제도 마찬가지로 파일을 표준 입력의 띠에 이어 준다 — 그래서 키보드를 두드리는 사람이 없어도 예제가 재현된다. 26장에서 “입력은 키보드에서 온다”는 오개념을 깰 때, 그 산 증거가 바로 이 책의 제작 과정이다.

문. 요즘 프로그램은 창과 버튼으로 소통하는데, 스트림은 옛날이야기 아닌가?

답. 화면의 앞쪽만 보면 그렇게 보이지만, 뒤쪽에서는 스트림이 오히려 전성기다. 서버 프로그램들의 기록(로그)은 스트림이고, 프로그램과 프로그램을 이어 붙이는 파이프라인도, 인공지능 챗봇이 답을 한 글자씩 “흘려보내는” 것도 스트림이다. 무엇보다 프로그래밍을 배우는 자리에서는 스트림이 가장 단순하고 정직한 소통로다 — 이 책의 예제가 전부 스트림 위에서 도는 이유다.

표현의 사다리가 완성됐다. 사물함에 수를 담는 법(7장), 글자를 담는 법(8장), 그리고 글자를 흘려보내는 법(9장)까지 — 이제 기계에 무엇이든 담고 내보낼 수 있다.

다음 장부터는 이 부의 마지막 등반이다. 먼저 이 띠를 누가 쥐어 주는지 — 프로그램이 실행된다는 것이 무슨 일이고 이 띠가 어디서 오는지(10장)를 보고, 그다음 지금까지의 단순한 기계 그림이 사실 얼마나 과감한 거짓말이었는지 — 기억이 갈라지고(12장), 실행이 겹쳐지고(13장), 컴파일러가 끼어들면서(14장), C가 왜 “추상적인 언어”일 수밖에 없는지(15장)의 이야기가 남았다.