표준입출력(1) - Stream
1. Stream을 사용하는 이유
(배경) 프로그램은 정보를 전달 하고, 정보를 전달받는 작업의 연속이다.
컴퓨터는 수많은 입출력(I/O) 장치(Keyboard, Mouse, Monitor, Printer)와 정보를 주고받기 위해 연결된다.
(문제점) 다양한 입출력(I/O) 장치가 서로 다른 정보 전달 기술을 사용한다면, 이를 호환시키는 것은 너무 어려운 일이다.
애플이 범용 C타입을 규격을 사용하지 않고 8핀, 12핀 독자 규격을 사용할때 겪었던 불편함과 같다.
여러분은 미터, 리터 가 아닌 feet, ml, lb 단위가 익숙한가?
스마트폰 터치패드 키보드로, PC 윈도우를 다룰 수 있는가?
(해결책) 입출력 표준 기술, Stream
현대 체계는 이러한 문제를 '표준화' 로 해결해오고 있다.
C언어와 Unix/ Linux는 현대 컴퓨터의 표준화된 개념에 가장 많은 영향을 끼쳤다.
8bit == 1Byte 체계처럼 입출력 (I/O) 장치들 간의 호환성도 C언어와 Unix/ Linux 방식이 절대적인 영향을 주었다.
1970 "모든 것을 파일로 취급한다" (Everything is a file) - Linux/Unix 핵심 철학
- 터미널, 디스크 파일, 프린터, 마우스, 네트워크, 소켓, 파이프, 시그널 등도 "파일"로 간주
- open(), read(), write(), close() 와 같은 동일한 시스템 콜(System Call) 함수를 사용
- ★개발자는 데이터가 키보드에서 오는지, 파일에서 오는지 신경 쓸 필요 없이 동일한 방식으로 프로그래밍
FILE 구조체 내부
typedef struct {
unsigned char *_base; /* 버퍼 시작 주소 */
size_t _size; /* 버퍼 크기 */
size_t _cnt; /* 남은 바이트 수 (읽기/쓰기) */
int _fd; /* 커널 파일 디스크립터 번호 */
unsigned int _flags; /* EOF, ERR, 읽기/쓰기 모드 등 */
// ... 플랫폼별 잠금·함수 포인터
} FILE;
1989 표준 I/O 라이브러리(stdio.h)
- C 언어가 (Unix뿐 아니라) 다른 OS로도 포팅되면서 휴대성 우선 설계.
- 1989 ANSI C & 1990 ISO C가 스트림/버퍼 규칙을 명문화 → 언어, OS 독립 인터페이스가 정립.
1988 POSIX 표준
- Portable Operating System Interface for UniX 이식 가능 운영 체제 인터페이스
- Unix 호환 계열뿐 아니라 Windows, VMS, 임베디드 RTOS에서 부분 호환 → 사실상 세계 표준.
(장점)
- 그렇기에 C언어 프로그래머는
개별 입출력 장치의 기능과 내부 동작 모르더라도, 개발 업무에 stream 이라는 한가지 능력만 기억하면 된다. - 운영체제는 입출력 장치의 종류에 상관없이, 표준 입출력 스트림 으로 항상 똑같은 절차를 통해 데이터를 읽고 쓴다.
- 그래서 네트워크 데이터 송/수신 함수가 파일 입출력 함수와 동일한 read, write 이다.
(윈도우의 경우, 방식이 미세하게 상이하여 소켓(WinSock)은 send, recv(receive) 함수를 사용한다.)
2. Stream 이해
Stream이 뭔가요?
- 데이터가 순차적으로 송수신되는 흐름을 체계화시킨 기술입니다.
- 영어로 '흐르는 시냇물' 이며, 가상 연결 통로에서 바이너리 데이터가 시냇물처럼 흐르는 모습을 의미한다.
- 끊기지 않고 연속적으로 "데이터가 흐르는 과정"을 추상화(구현) 한 용어다
- Data 가, Source(원본)에서 Destination(목적지)로 Block 단위로 전달되는 과정을 구현(추상화)한 기술을 의미한다.
- printf, scanf 함수의 내부 구현 구조를 모르더라도 사용할 수 있다. stream도 마찬가지다
- stream 내부 구현 구조를 모르더라도, stream 이라는 것을 사용하는 것만으로, 키보드에서 입력받은 Data 를 파일이나, 모니터로 내보낼 수 있다.
- 하지만, prinf 매개변수, 정렬, 동작 방식을 이해해야 하는 것처럼
stream 도 내부 구현 구조를 모르더라도, 동작 방식에 대한 약간의 이해가 있어야 좀 더 효율적인 사용이 가능하다.
- stream 내부 구현 구조를 모르더라도, stream 이라는 것을 사용하는 것만으로, 키보드에서 입력받은 Data 를 파일이나, 모니터로 내보낼 수 있다.
학습관점
출력을 위해, 출력 개념과 printf( ) 함수 사용법을 배웠다
입력을 위해, 입력 개념과 scanf( ) 함수 사용법을 배웠다.
Stream을 사용하기 위해서는, 1)Stream의 개념과, 2)버퍼(buffer)라는 도구 동작 방법을 약간 깊게 학습해야 한다.
Stream 은 자동차 운전 방법, 한식 요리법, 정장, 우산 과 같은개념적 모델, 방법론이고,
Buffer 는 그 흐름을 실제로 성능 좋게 구현하기 위한 기술적 장치, 실제 소스코드로 다루어지는 도구입니다.
구분 | 스트림 (Stream) - 개념/목표 |
버퍼 (Buffer) - 기술/도구
|
비유 | 된장찌개 요리법 전체 |
요리에 사용하는 '도마' 또는 '믹싱 볼'
|
역할 | 무엇(What)을 할 것인가? (데이터를 일관된 방식으로 입출력한다) |
어떻게(How) 효율적으로 할 것인가?
(입출력 횟수를 줄여 성능을 높인다) |
관점 | 사용자/프로그래머의 관점 (추상화된 편리한 인터페이스) |
시스템/엔지니어의 관점
(성능 최적화를 위한 내부 구현) |
핵심 가치 | 추상화, 일관성, 이식성 |
성능, 효율성, 최적화
|
3. Stream 동작
3.1 Stream 입출력 예시 1

계층 | 핵심 역할 | 주요 기술 |
1. 애플리케이션 (사용자 공간) |
고수준의 쉬운 인터페이스 제공 |
C 표준 라이브러리 (stdio), FILE 구조체,
사용자 버퍼링 |
2. 운영체제 커널 (커널 공간) |
장치 추상화 및 데이터 중개 |
시스템 콜 (System Call), 파일 서술자 (File Descriptor),
커널 버퍼 캐시 |
3. 디바이스 드라이버 (커널 공간) |
하드웨어 제어 신호로 변환 |
특정 하드웨어(그래픽카드, 디스크 등)를 위한 소프트웨어 인터페이스
|
4. 하드웨어 | 물리적 데이터 입출력 수행 |
CPU, 메모리, 그래픽 카드, 모니터 등 물리적 장치
|
3.2 Stream 입출력 예시 2 (표준 입출력)




4. stream 사용
4.1 C언어, 표준 입출력 stream 사용 방법
- C언어, <stdio.h> 라이브러리를 사용해 표준 입출력을 사용한다.
printf( ) stdout stream
scanf( ) stdin stream
4.2 C++, 표준 입출력 stream 사용 방법
- C++, <iostream> 라이브러리를 사용해표준 입출력을 사용한다.
cin 객체
cout 객체
4.3 C언어, FILE stream 사용 방법
- 파일 입출력은, 표준 스트림 처럼 자동으로 Stream이 생성되지 않는다.
File* 라는 Stream을 의도적으로 생성시켜 사용한다.
그래서 의도적으로 생성한 stream 은 필수로 닫아줘야 한다. - File* fp = fopen("파일.txt", "w");
fp 라는 file stream 을 생성한 뒤 사용한다.
fprintf( fp , "Hello world\n");
fscanf( fp , " ");
4.4 소켓 stream 사용 방법
- Network 통신에서 Socket 으로 통신하면 socket stream 이 자동 생성된다.
- Socket 꼭! 닫아야 하는 이유는 stream을 닫아야 하기 때문이다.
- Database 도 마찬가지의 경우다.
4.5 Qt Stream 사용 방법
- Qt i/o 공식문서 링크
- Qt 네트워크에서 파일 송수신 방법
- QDataStream 을 socket에 연결한다. (로컬 파일 연결 작업과 동일)
- QByteArray 타입의 데이터를 stream으 주고 받으면 된다.
5. Linux 표준 입출력 Stream의 물리적 위치

- 리눅스의 경우, 최상위 경로에서 dev 디렉토리에서 확인 가능하다.
운영체제가 만들어 놓은 stdin, stdout, stderr 스트림을 확인 할 수 있다. - 위 이미지에서 오른쪽 0, 1, 2 번호는 파일 디스크립터(File Descriptor)이다.
- 프로세스(프로그램 실행)가 시작되면, stdin, stdout, stderr 정보와 함께 실행된다.
- File* 를 사용하면 File Stream이 생성되고 File Descriptor를 부여받는다.
- 아래 stream 모두 File Descriptor 라는 고유 정수 식별값을 부여받는다.
- 입력
- 출력
- 파일
- 프린터
- 모니터
- 데이터베이스
- 소켓
- ...
- 아래 stream 모두 File Descriptor 라는 고유 정수 식별값을 부여받는다.
- Socket 이 연결되면 Socket Stream이 생성되고 File Descriptor 를 부여받는다.
즉, 서버에서 연결된 Socket 들을 fd(File Descripor) 정수값으로 로 식별하여 Client를 구분 할 수 있다. - Qt 에서 Socket의 File Descriptor 확인 방법
- socket->socketDescriptor()
- QString::number(socket->socketDescriptor())
- File Descriptor 값은 stream에 부여되는 중복되지 않는 고유한 정수값으로 Socket 의 식별이 가능하다.
6. 파일 디스크립터(File Descriptor)

리눅스에서 파일 디스크립터(File Descriptor)란, C 언어와 같은 프로그래밍 언어가 아닌 시스템(OS)에 의해 관리되는 일련의 정수값으로 파일 또는 소켓의 식별에 사용된다.
(*윈도우는 파일과 소켓을 구분한다.)
▶ UNIX에서 파일을 새로 열면(open) int 타입의 정수를 리턴하는데, 이는 파일 기술자 테이블(file descriptor table) 의 index 번호다.
▶ 예를들어, 2개의 파일을 open하면 파일 기술자는 3과 4가 배정되며, 소켓을 생성하면 파일 기술자와 똑같은 기능과 역할을 하는 소켓 기술자(socket descriptor)가 리턴된다. 즉, 파일과 소켓이 기술자 테이블을 공유한다.
▶ 단순하게 설명하자면, 단체 생활에서 이름보다 번호를 사용하는 것이 편한것과 같다.

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/socket.h>
int main(void)
{
int fd1, fd2, fd3;
fd1=socket(PF_INET, SOCK_STREAM, 0);
fd2=open("test.dat", O_CREAT|O_WRONLY|O_TRUNC);
fd3=socket(PF_INET, SOCK_DGRAM, 0);
printf("file descriptor 1: %d\n", fd1); // file descriptor 1: 3
printf("file descriptor 2: %d\n", fd2); // file descriptor 1: 4
printf("file descriptor 3: %d\n", fd3); // file descriptor 1: 5
// 3 부터 시작하는 이유는 0,1,2 는 표준 입출력에 할당되었기 때문이다.
// 아래 getchar(); 는 이 프로그램이 종료되지 않을 때 사용.
//getchar();
close(fd1);
close(fd2);
close(fd3);
return 0;
}
7. 실행중인 프로그램(Process)의 Stream과 File Descriptor
7.1 리눅스 프로세스 확인 방법
- 리눅스에서 실행중인 프로세스를 확인하는 방법은 아래와 같다.
ps au

7.2 실행 파일명으로 프로세스 아이디(PID) 찾기
- 실행중인 프로세스의 스트림을 확인하기 위해, 프로세스 아이디를 찾는다.
ps au | grep 실행파일명

이전 예제에서, getchar 부분의 주석을 제거하고 실행하여, 입력 전까지 프로세스가 종료되지 않도록 한다.
실행중인 프로세스의 아이디(PID)를 찾는다.
7.3 PID 경로에서 fd(File Descriptor) 확인
- 실행중인 프로세스 아이디 디렉토리로 이동하여, fd 경로에서 리스트를 확인하면, 실행중인 프로그램 내에서 생성한 file descriptor 리스트를 확인 할 수 있다.
cd /proc/프로세스아이디/fd
ls -al

표준 입출력의 파일 디스크립터는 프로세스가 실행되면 공유된다.
프로그램이 실행되어 생성한 파일 디스크립터(소켓2개, 파일1개)는 3,4,5 번의 스트림을 생성하고 프로세스 고유 fd를 할당받았다
8. 저수준 vs 고수준 vs 프레임워크(응용)
저수준(FD)
/* 1) open() 예시: 로그 파일에 한 줄 쓰기 */
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int fd = open("log.txt",
O_WRONLY | O_CREAT | O_APPEND,
0644); // rw-r--r--
const char *msg = "hello\n";
write(fd, msg, strlen(msg));
close(fd);
고수준(FILE*)
/* 2) fopen() 예시: 텍스트 파일 읽고 화면에 출력 */
#include <stdio.h>
FILE *fp = fopen("data.txt", "r");
char buf[256];
while (fgets(buf, sizeof buf, fp))
fputs(buf, stdout);
fclose(fp);
- fopen()은 결국 내부에서 open()을 호출하고 반환된 식별자 int fd를 포함한 로직
- fopen() >> 내부에서 open() 시스템 호출 실행 >> fd 를 얻어 FILE 구조체에 저장
- fopen()은 fd를 FILE 구조체에 넣어 버퍼와 상태(flag)를 함께 관리
- 반대로 이미 열린 fd를 FILE*로 감싸고 싶다면 fdopen(fd, "r")를 사용
- fopen() → open() : 항상 발생 (자동)
- open() → fopen() : 자동으로 일어나지 않으며, 필요하면 fdopen()을 명시적으로 호출해야 한다.
int fd = open("data.log", O_WRONLY | O_CREAT | O_APPEND, 0644);
if (fd == -1) perror("open");
FILE *fp = fdopen(fd, "a"); // 여기서부터는 fprintf 등 사용 가능
fprintf(fp, "로그 한 줄\n");
fclose(fp); // fclose가 close(fd)까지 처리
정리
계층 | 핵심 특징 | 파일 I/O 예시 | TCP/소켓 I/O 예시 | 대표 라이브러리·함수 |
저수준 (커널 시스템콜 래퍼) |
FD(정수) 를 직접 다룸버퍼 없음·원자적 I/O·플래그 제어 | open / read / write / lseek / close | socket / connect / send / recv / closebind / listen / accept | POSIX libc(syscall 래퍼), Winsock, fcntl, ioctl |
고수준 (버퍼, 포맷 스트림) |
사용자 공간 버퍼링·문자열 포맷 기능FD ↔ FILE* 변환 가능 | fopen / fread / fwrite / fclosefprintf / fscanf | fdopen(sock_fd,"r+") → FILE*fprintf, fgets, getline 등 | C stdio 계열, C++ <fstream>·<iostream> |
최상위 프레임워크 or 응용(Application-level) (프로토콜 이벤트 추상화) |
프로토콜 처리·이벤트 루프·스레드풀·TLS 등까지 캡슐화비동기·재시도·타임아웃 내장 | (대개 파일은 표준 I/O로 충분) | HTTP/2, WebSocket, gRPC, TLS, 비동기 이벤트 등 고차 기능 | 네트워크: libcurl, Boost.Asio, libevent, libuv, gRPC, cpp-httplib, OpenSSL BIO, Python socketserver 멀티 I/O: Tokio(Rust), .NET Sockets, Node.js net |
간단 예시
/* 1) 저수준: 소켓 생성 & 연결 */
int sock = socket(AF_INET, SOCK_STREAM, 0);
connect(sock, (struct sockaddr *)&srv, sizeof srv);
/* 2) 고수준: fdopen 으로 FILE* 래핑 */
FILE *net = fdopen(sock, "r+"); // now buffered & printf-friendly
/* 3) 포맷 I/O 사용 - 전송*/
fprintf(net,
"GET / HTTP/1.0\r\nHost: example.com\r\n\r\n");
fflush(net); // 버퍼(비우기) == 커널로 전송
/* 4) 포맷 I/O 사용 - 수신*/
char line[256];
while (fgets(line, sizeof line, net))
fputs(line, stdout);
/* 5) 종료*/
fclose(net); /* fclose 가 close(sock) 까지 처리 */
- 1) 저수준 단계
socket()이 정수 FD를 만들고, connect()로 TCP 세션을 맺습니다. - 2) 고수준 승격
fdopen()이 FD를 FILE*로 감싸 버퍼·포맷 기능을 제공합니다. - 3) 포맷 I/O
fprintf()는 stdio 버퍼에 쌓이고, fflush() 때 write()로 커널 송신 큐에 복사됩니다. - 4) 수신
fgets()는 필요할 때마다 read()로 커널 수신 큐에서 데이터를 가져옵니다. - 5) 종료
fclose()가 남은 버퍼를 flush한 뒤 close()를 호출해 소켓을 닫고, TCP FIN을 전송합니다.
고수준 코드는 fprintf·fgets처럼 간결한 포맷 I/O를 쓰고,
필요하다면 언제든 저수준 FD API( send/recv , setsockopt 등)를 병용