표준 스트림, 표준 입출력에 대해 알아보자

표준 스트림, 표준 입출력에 대해 알아보자

2018, Oct 15    

들어가며


프로그래밍에서 ‘표준 입출력’, ‘표준 스트림’에 대한 이야기를 들을 때가 있다. 주로 C 계열 언에서 파일을 다룰 때인데 stdin, stdout , 과 같은 용어를 본 적이 있을 것이다. 각각 ‘standard input’, ‘standard output’의 약자인데 입력과 출력이야 키보드, 모니터일 것 같긴한데 ‘표준’이 뭔지 한동안 의아했었다.

또 표준 입출력의 개념은 하드웨어단이 상당히 추상화된 파이썬에서도 찾아볼 수 있는데 파이썬을 배울 때 가장 처음 배우는 print 함수를 헬핑(helping)해보면 file 인자에 ‘sys.stdout’이라고 적혀 있는 것을 볼 수 있다.

>>> help(print)


print(value, ..., sep=' ', end='\n', file=sys.stdout, flush=False)

Prints the values to a stream, or to sys.stdout by default.
...

하지만 표준 입출력에 대해 그냥 키보드 입력, 모니터 출력이라고 알고 있는 경우가 많다. 나도 그러했고. 그러나 프로그래머라면, 특히 ‘유닉스 쉘’을 쓰는 사람이라면 쉘의 기능을 최대한 이끌어내기 위해서는 표준 입출력에 대해 꼭 알고 있어야 한다. 사실 이 포스트를 작성하는 이유도 향후 shell programming 쪽 개념을 포스트할 때를 위해서인데, redirection과 같은 입출력과 관련한 내용을 이해하기 위해서는 표준 입출력에 대한 이해가 필수적이기 때문이다.

만약 유닉스 쉘을 쓴다면 이 포스트가 향후 도움이 될 수 있다.

표준 스트림


표준 스트림이란?

표준 스트림은 ‘Standard Stream’ 의 약자로 컴퓨터 프로그램에서 ‘표준적으로’ 입력으로 받고 출력으로 보내는 데이터와 매체를 총칭하는 용어이다. 우리는 꼭 프로그래밍이 아니더라도 컴퓨터 생활을 하면서 수많은 프로그램들을 사용한다. 그 프로그램들은 많은 경우 정해진 어떤 ‘입력’을 받으며 많은 경우 정해진 어떤 형태의 ‘출력’을 내놓는다. 프로그램이 입력을 받지 않고 반환하는 출력이 없다면 우리가 컴퓨터를 사용하는 이유도 없을 것이다. 하는 일이 없으니까. 일단 논의를 쉽게 하기 위해 이 포스트에서 말하는 프로그램은 ‘unix shell’로 놓고 생각하자. 쉘 또한 사용자의 입력을 키보드로 받아 실행하고 결과를 콘솔에 출력하는 프로그램이다. 아래 사진에서는 ‘키보드’로 ‘apropos’ 명령을 ‘입력’ 받아 ‘콘솔’에 ‘출력’하고 있다.

shell

하지만 이런 한 줄의 정의로는 표준 스트림을 이해하기 쉽지 않다. 컴퓨터 과학에서 개념을 제대로 이해하기 위해서 용어를 분석하는 것이 때때로 먹힐 때가 있다.

표준 스트림에서 표준과 스트림이라는 단어를 나누어서 생각해보자.
먼저 스트림(stream). 스트림은 프로그램을 드나드는 데이터를 바이트의 흐름으로(byte stream) 표현한 단어이다. 수 십년 전 컴퓨터를 쓸 때도 프로그램의 입력과 출력을 다뤄야 했을 것이다. 입력이 있어야 명령을 내리고 출력이 있어야 그 결과를 확인할테니까 말이다. 하지만 당시는 프로그램의 입력과 출력을 지정하는 일이 운영체제 쪽과 얽혀 어렵고 반복작업해야 하는 지루한 일이었다. 많은 운영체제에서 이를 위해서는 환경설정 정보, 내부 파일 목록 정보를 일일히 지정해주거나 천공카드, 일반 디스크, 전자 테이프 등 하드웨어 관련한 설정을 세세히 해줘야 했기 때문이다.

유닉스에서는 이런 번거로움을 해결하기 위해 장치를 추상화해서 각 장치를 파일처럼 다루는 것으로 이 문제를 해결했다. 다양한 하드웨어 장치별로 입력과 출력을 위한 설정작업을 따로 하는 것이 아니라 파일을 읽고 쓰는 한 가지 작업으로 통일된 것이다.
그리고 그 파일에서 읽히고 나가는 데이터를 stream 이라고 정의했다. 실제로 리눅스에서는 ‘/dev’ 디렉토리가 추상화한 장치들을 파일 형태로 담고 있는데 그 안에 stdin , stdout 등이 있는 것을 확인할 수 있다.


다음은 ‘표준’에 대해 알아보자. 모든 프로그램은 ‘많은 경우’ 입력과 출력을 필요로 한다. 아까부터 굳이 ‘많은 경우’라는 단어를 집어넣는 것은 때때로 입력 또는 출력 둘 중 하나가 생략되는 프로그램 실행도 가능하기 때문이다. 예를 들어 ‘ls’ 라는 프로그램을 실행할 때(이것도 프로그램이다.) 명시적으로 경로 인자를 입력으로 줄 수 있지만 생략하는 것도 가능하다.

다시 돌아와서, 모든 프로그램이 입력과 출력을 필요로 하는데 어떤 프로그램에 있어 만약 대부분의 입력과 출력이 한 출처로부터만 발생한다면 사용자가 명시하지 않는 이상 기본적으로 사용할 입력과 출력을 프로그램 개발 시에 지정할 수 있으면 좋을 것이다. 이렇게 한 프로그램이 기본적으로 사용할 입출력 대상을 ‘표준 입출력’이라고 한다.

우리의 예제 프로그램 ‘쉘’은 키보드 입력을 표준 입력으로 하고 모니터 콘솔 출력을 표준 출력으로 한다. 또한 유닉스에서는 따로 명시되지 않는 한 표준 입출력 대상은 부모 프로세스로부터 상속 받는다. 쉘 상에서 ‘ls‘라는 프로그램을 실행하면 이 프로그램은 쉘 프로세스의 자식 프로세스로 실행이 되는데 이때 표준 출력을 상속 받아 쉘과 같은 콘솔에 결과를 반환하는 것이다.

standard-stream


그리고 표준 입출력은 표준 입력과 표준 출력으로 나뉘고 표준 출력은 표준 출력와 표준 에러로 나뉜다. 각 프로세스는 초기화될 때 세 가지 스트림이 설정되는데 이 표준 출력, 표준 입력, 표준 에러가 그것이다. 각각에 대해 좀더 살펴보자.


표준 입력

표준 입력(Standard input)은 프로그램에 입력되는 데이터의 표준적인 출처(장비나 파일)를 일컬으며 stdin 으로 줄여 표현한다. 유닉스 쉘에서는 표준 입력이 키보드로 설정되어 있다.

리눅스에서 표준 입력은 file descriptor(파일을 고유하게 구별하는 식별자) 가 0으로 설정되어 있다. 프로그래밍 언어에서는 매직 넘버를 피하기 위해 상수로 할당되어 있으며 POSIX C의 <unistd.h>에서는 STDIN_FILENO, 이후 C의 <stdio.h>에서는 FILE* stdin, C++ <iostream>에서는 std::cin으로 지정되어 있다. 또 우리의 사랑 파이썬에서는 sys모듈의 stdin이라는 변수로 접근할 수 있다.


표준 출력

표준 출력(Standard output)은 프로그램에서 출력되는 데이터의 표준적인 방향(장비나 파일)을 일컬으며 크게 표준 출력(stdout)과 표준 에러(stderr)로 구분할 수 있다. 유닉스 쉘에서는 표준 출력, 표준 에러 모두 콘솔로 설정되어 있다.

표준 출력은 정상적인 출력이 반환되는 방향을 말하고, 표준 에러는 프로그램의 비정상 종료 시에 반환되는 방향이다. 프로그램이 정상적으로 종료하면 사용자가 바라던 형태의 출력 결과가 나올 것이다. 하지만 실행 시에 어떠한 장애를 만나 비정상적으로 종료하면 보통 에러 메시지를 반환하게 된다.

유닉스 쉘의 ‘cat’ 으로 파일을 읽는 명령어를 보내면 파일이 있을 경우 정상적으로 출력되지만(표준 출력), 입력 받은 이름의 파일이 없다면 에러 메시지를 출력한다.(표준 에러)

$ cat nofile


cat: nofile: 그런 파일이나 디렉터리가 없습니다

리눅스에서 표준 출력은 file descriptor가 1로 설정되어 있다. 그리고 POSIX C의 <unistd.h>에서는 STDOUT_FILENO, 이후 C의 <stdio.h>에서는 FILE* stdout, C++ <iostream>에서는 std::cout으로 지정되어 있다. 파이썬에서는 sys모듈의 stdout이라는 변수로 접근할 수 있다. 이제 우리는 맨 위의 파이썬 help(print) 에서 왜 file 이 디폴트로 sys.stdout으로 되어 있는지 이해할 수 있다. 바꿔 말하면, 저 file 인자에 다른 값, 예를 들면 실제 파일을 넣으면 print 를 통해 실제 파일에 글을 쓸 수도 있다는 것을 뜻한다.

마지막으로 표준 에러의 file descriptor는 2로 설정되어 있다. POSIX C의 <unistd.h>에서는 STDERR_FILENO, 이후 C의 <stdio.h>에서는 FILE* stderr, C++ <iostream>에서는 std::cerr, 또는 std::clog로 지정되어 있다. 파이썬에서는 sys.stderr로 할당되어 있다.

출력을 정상적인 출력과 에러 출력으로 구분한다는 뜻은 이 둘의 출력 방향을 다르게 할 수 있다는 뜻이 된다. 같은 프로그램에서 같은 입력의 실행결과에서 정상적인 출력은 ‘A’ 파일에, 에러 출력은 ‘B’ 파일에 따로 담는 것이 가능해지는 것이다. 서버 프로그래밍을 할 때 일반적인 접속 로그는 access.log에, 에러 로그는 error.log에 담기는 것을 우리는 이제는 이해할 수 있다. 자세한 방법은 향후 ‘쉘 프로그래밍’ 카테고리에서 좀더 자세히 다룬다.


마치며


이번 포스트에서는 표준 스트림, 표준 입출력에 대해 다루었다. 표준 스트림은 프로그램이 기본적으로 사용하는 입력과 출력의 매체(파일이나 장치)이며 표준 입출력을 총칭한다. 표준 입출력은 표준 입력과 표준 출력을 합친 말로 각각 표준적인 입력과 출력의 매체를 일컫는다. 이때 표준 출력은 프로그램의 정상적인 출력을 반환하는 표준 출력과 에러 발생시 에러 메시지를 출력하는 표준 에러로 다시 나뉜다.

아까도 말했지만 이번 포스트는 쉘 프로그래밍으로 가기 위한 개념 다지기 편이다. 표준 입출력은 사용자가 다른 입출력 매체를 지정하지 않았을 때의 기본 입출력 매체라고 이야기했다. 그 얘기는 사용자가 다른 입출력 매체를 입력하면 입출력의 방향을 표준이 아닌 임의의 방향으로 바꿀 수 있다는 것이 된다. 아까 봤던 ‘cat: nofile: 그런 파일이나 디렉터리가 없습니다’라는 에러 메시지가 콘솔이 아닌 파일에 담기게도 할 수 있다.

이 개념을 redirection이라고 하며 쉘 프로그래밍이 정말로 강력해지고 재밌어지는 부분은 여기서부터 시작이라고 생각한다. 출력과 입력을 돌리고 꼬아 GUI에서는 생각지도 못했던 재밌는 기능들을 많이 써볼 수 있다.


멀지 않은 훗날, 이번 포스트를 통해 함양한 표준 입출력에 대한 이해를 바탕으로 쉘의 진정한 세계로 항해해 나가자 :smile:

출처