스트림
고친 사람 github-actions[bot]
스트림은 데이터를 한꺼번에 넘기지 않고 앞에서부터 조금씩 흘려보냅니다. 받는 쪽은 전체 길이를 몰라도 도착한 만큼 떼어 처리를 시작합니다. 파일을 읽는 통로도 이 이름으로 부릅니다. 연결 하나 안에 난 갈래도 같은 이름입니다.
쉽고 빠른 이해
스트림은 데이터를 앞에서부터 조금씩 흘려보내는 통로입니다. 커다란 로그 파일을 앞줄부터 읽어 가며 세는 것이 그렇습니다.
이게 없으면 데이터가 다 모일 때까지 기다렸다가 한 덩어리로 다뤄야 합니다. 길이를 아직 모르거나 끝이 없는 데이터는 그 방식으로 시작조차 못 합니다.
어떻게 도는가:
- 내보내는 쪽이 앞에서부터 데이터를 밀어 넣습니다
- 받는 쪽이 원하는 만큼 떼어 처리하고 그만큼 놓아 줍니다
- 더 없다는 신호가 오면 끝냅니다
대가는 지나간 데이터를 다시 못 본다는 것입니다. 되돌아가 보려면 따로 담아 둬야 합니다.
상세
떡집에서 가래떡 뽑는 것을 떠올려 봅시다. 기계에 반죽을 몇 덩이 나눠 넣었든 앞으로는 끊김 없는 한 가닥이 계속 밀려 나옵니다. 받는 사람은 다 나오기를 기다리지 않고 원하는 길이에서 잘라 담습니다.
스트림은 데이터를 앞에서부터 차례로 흘려보내는 통로이자, 그 위를 지나가는 데이터 자체입니다. 커다란 파일을 한 줄씩 읽어 가며 세는 일, 소켓으로 들어오는 바이트를 도착하는 대로 받아 넘기는 일이 그런 흐름입니다.
스트림이 약속하는 것
스트림이 무엇을 지키고 무엇을 안 지키는지는 이렇게 갈립니다.
| 약속 항목 | 스트림에서 |
|---|---|
| 순서 | 내보낸 순서대로 도착합니다 |
| 읽는 방향 | 앞에서 뒤로 한 방향입니다 |
| 전체 길이 | 미리 알려 주지 않습니다 |
| 지나간 데이터 | 다시 주지 않습니다 |
| 끝 | 더 없다는 신호로 알립니다 |
뒤의 세 줄이 처음 쓰는 사람을 걸리게 합니다. 길이를 모르니 미리 그만큼 그릇을 잡아 둘 수 없습니다. 한 번 읽고 넘어간 데이터도 되짚어 볼 수 없습니다.
읽는 쪽 코드는 그래서 대개 같은 모양입니다. 원하는 크기만큼 떼어 오고, 빈 조각이 오면 멈춥니다.
조각 = 스트림.읽기(4096) // 4096바이트만
while 조각: // 비면 끝이다
처리(조각)
조각 = 스트림.읽기(4096)
파일이 아무리 커도 메모리에 올라오는 것은 조각 하나뿐입니다.
block-beta columns 6 a["읽고 지나간 조각"]:2 b["지금 메모리에 있는 조각"]:1 c["아직 안 읽은 나머지"]:3
파일과 소켓을 읽는 창구
운영체제는 파일도 소켓도 파이프도, 글자를 주고받는 터미널도 같은 방식으로 다루게 해 줍니다. 열고, 앞에서부터 읽거나 쓰고, 다 되면 닫습니다. 유닉스 계열에서 그 통로를 가리키는 작은 정수가 파일 디스크립터입니다.
응용 코드는 통로 뒤에 무엇이 붙어 있는지 몰라도 됩니다. 로그를 파일로 쓰던 코드에 파이프를 대신 꽂아도 읽고 쓰는 줄은 바뀌지 않습니다.
flowchart TD
subgraph 원천["통로 뒤에 붙는 것"]
F["파일"]
S["소켓"]
P["파이프"]
T["터미널"]
end
원천 --> ST["스트림 · 열고 읽고 닫는다"]
ST --> APP["손대지 않은 응용 코드"]
그림의 네 가지는 저장 방식도 속도도 다릅니다. 응용 코드가 보는 모양이 같을 뿐입니다.
언어 라이브러리도 같은 모양을 얹습니다. 자바는 InputStream과 OutputStream 같은 이름으로, C 표준 라이브러리는 파일 포인터로 이 통로를 감쌉니다.
덩어리째 주고받는 방식과 갈리는 대목
스트림으로 다루면 보내는 쪽이 몇 번에 나눠 썼는지가 받는 쪽에 남지 않습니다. 이어진 한 줄만 도착합니다. 이렇게 경계가 사라진 바이트 줄을 바이트 스트림이라고 따로 부릅니다.
TCP(Transmission Control Protocol, 전송 제어 프로토콜) 연결이 그렇습니다. 세 번에 나눠 보낸 데이터가 받는 쪽에서 한 번에 읽히기도 하고 둘로 쪼개져 읽히기도 합니다.
flowchart TD
subgraph 보낸쪽["보낸 쪽 · 세 번에 나눠 씀"]
A1["ABCD"]
A2["EF"]
A3["GHIJ"]
end
subgraph 스트림["스트림 · 이어진 한 줄"]
B1["ABCDEFGHIJ"]
end
subgraph 읽은쪽["읽은 쪽 · 두 번에 읽힘"]
C1["ABCDEFG"]
C2["HIJ"]
end
보낸쪽 --> 스트림
스트림 --> 읽은쪽
같은 바이트 줄인데 끊어 읽는 지점이 보낸 쪽과 다릅니다.
맞은편에는 덩어리를 그 모양대로 나르는 방식이 있습니다. UDP(User Datagram Protocol, 사용자 데이터그램 프로토콜)로 보낸 데이터그램 하나는 한 번 보낸 것이 한 번에 읽힙니다. 대신 도중에 빠져도 다시 보내 주지 않습니다.
| 스트림으로 다룰 때 | 덩어리로 다룰 때 | |
|---|---|---|
| 읽는 단위 | 읽는 쪽이 정합니다 | 보낸 쪽이 정합니다 |
| 덩어리 경계 | 남지 않습니다 | 남습니다 |
| 길이 | 미리 몰라도 됩니다 | 한 덩어리 크기가 정해집니다 |
표의 가운데 줄이 실무에서 자주 걸리는 대목입니다. 스트림 위로 덩어리를 주고받으려면 사라진 덩어리 경계를 읽는 쪽이 다시 세워야 합니다. 이 경계를 메시지 경계라고도 부릅니다. 덩어리 앞에 길이를 먼저 적어 두거나, 약속한 구분 바이트가 나오면 끊는 식입니다.
분야마다 가리키는 것
스트림이라는 말은 여러 분야가 같이 씁니다. 가리키는 대상은 다르지만 「앞에서부터 차례로 흐른다」는 점은 같습니다.
| 분야 | 스트림이 가리키는 것 |
|---|---|
| 입출력 | 파일과 소켓을 앞에서부터 읽고 쓰는 통로 |
| 전송 | 경계 없이 이어져 흐르는 바이트 줄 |
| 연결 다중화 | 연결 하나 안에 난 갈래 하나 |
| 데이터 처리 | 끝나지 않고 계속 들어오는 데이터 |
| 암호 | 바이트를 하나씩 이어 가며 암호화하는 방식 |
셋째 줄은 앞의 둘과 달리 통로가 하나가 아닙니다. 앞 절의 통로가 파일이나 소켓 하나로 이어지는 창구였다면, 이것은 그 연결 하나 안에 다시 난 갈래입니다. 연결 하나 안에 갈래를 여러 개 내는 프로토콜이 그 갈래 하나를 스트림이라고 부릅니다.
HTTP(HyperText Transfer Protocol, 하이퍼텍스트 전송 프로토콜)의 둘째 판 HTTP/2가 그렇습니다. 요청과 응답 한 쌍이 스트림 하나를 씁니다. 스트림마다 번호가 붙습니다.
flowchart TD
subgraph 연결["연결 하나"]
S1["스트림 1"]
S2["스트림 2"]
S3["스트림 3"]
end
S1 --> M["번호를 단 조각들이 섞여 흐른다"]
S2 --> M
S3 --> M
M --> R["받는 쪽이 번호대로 다시 가른다"]
조각에 번호가 붙어 있으니 섞여 도착해도 제 갈래를 찾아갑니다. 이 뜻의 스트림에서 중요한 것은 끊김 없이 흐른다는 것이 아니라 서로 막히지 않는다는 것입니다. 한 갈래가 막혀도 나머지는 계속 흐릅니다.
넷째 줄은 데이터를 다루는 쪽의 말입니다. 주문이 들어오는 대로, 센서 값이 올라오는 대로 처리하는 방식을 스트림 처리라고 합니다. 데이터가 다 모인 뒤에 한꺼번에 도는 배치 처리와 맞세워집니다.
다섯째 줄은 스트림 암호입니다. 한 덩어리를 다 모아 놓고 한꺼번에 푸는 대신 바이트가 오는 대로 처리한다는 점이 같아 붙은 이름입니다.
스트림이 맞는 데와 안 맞는 데
데이터가 메모리보다 크거나, 길이를 아직 모르거나, 끝이 없을 때 이 방식이 맞습니다. 앞부분을 처리하는 동안 뒷부분이 도착하므로 첫 결과가 일찍 나옵니다. 영상을 다 내려받기 전에 재생이 시작되는 것이 그래서입니다.
데이터 전체를 여러 번 훑어야 하는 일에는 안 맞습니다. 정렬하거나 뒤에서부터 읽으려면 어차피 다 모아야 합니다. 지나간 데이터를 다시 보려고 따로 담아 두면 스트림으로 아낀 메모리를 도로 내놓는 셈입니다.
보내는 쪽과 받는 쪽의 속도 차
흘려보내는 쪽이 받는 쪽보다 앞서가면 처리되지 않은 데이터가 버퍼에 쌓입니다. 버퍼는 아직 처리하지 못한 데이터를 잠시 담아 두는 메모리 공간입니다. 쌓이기만 하면 메모리가 찹니다.
그래서 받는 쪽이 감당할 만큼만 보내라고 알리는 장치를 둡니다. 이것을 백프레셔라고 합니다. 데이터는 앞으로 가는데 이 신호만 거꾸로 거슬러 올라갑니다.
flowchart TD
SND["보내는 쪽"] --> BUF
subgraph BUF["버퍼 · 아직 처리 못 한 조각이 쌓인다"]
P1["조각"]
P2["조각"]
P3["조각"]
end
BUF --> RCV["받는 쪽"]
RCV -- "그만 보내라" --> SND
관련 항목
스트림을 읽고 쓰는 창구
파일 디스크립터 · 소켓 · 파이프 · 표준 입력 · 표준 출력 · InputStream · OutputStream
스트림을 실어 나르는 프로토콜
TCP · HTTP/2 · HTTP/3 · QUIC · TLS · gRPC · SCTP
스트림과 맞세워지는 전달 단위
데이터그램 · 메시지 · UDP · 메시지 경계 · 블록
스트림 위에 덩어리 경계를 다시 세우는 방법
프레이밍 · 길이 접두 · 구분자 · 청크 전송 인코딩 · 직렬화
스트림의 하위 종류
바이트 스트림 · 문자 스트림 · 제어 스트림 · 단방향 스트림 · 양방향 스트림 · 스트림 암호
연결 하나에 여러 스트림을 싣는 장치
다중화 · 프레임 · 스트림 ID · 스트림 오프셋 · 연결
스트림이 흐르는 속도를 맞추는 수단
흐름 제어 · 백프레셔 · 버퍼 · 버퍼링 · 슬라이딩 윈도
스트림을 값의 흐름으로 다루는 방식
스트림 처리 · 리액티브 스트림 · 이터레이터 · 제너레이터 · 파이프라인 · 배치 처리
스트림을 쓰다 만나는 장애
머리줄 막힘 · 패킷 손실 · 타임아웃 · 부분 읽기 · 메모리 누수
스트림을 품는 상위 분류
다른 이름: stream · streams