티스토리 뷰
LLM 시대에도 변하지 않는 기초: Engineer가 TCP/IP와 HTTP를 알아야 하는 진짜 이유 3편 - Socket, File I/O, TCP I/O, Buffered I/O: 패킷은 어떻게 코드가 되는가?
ebson 2026. 3. 6. 13:31LLM API를 호출하고 스트리밍 응답을 받는 일은 이제 일상이 되었습니다. 그러나 우리가 InputStream.read()를 호출하는 순간, 그 아래에서 어떤 계층과 버퍼를 거쳐 데이터가 올라오는지 설명해보면 생각보다 명확하지 않은 경우가 많습니다.
이 글에서는 소켓이 무엇인지, 파일 I/O와 TCP I/O는 어떻게 다른지, 커널 버퍼는 어떤 역할을 하는지, 그리고 packet·segment·frame은 무엇이 다른지 공식 문서 기반으로 다시 정리해보려 합니다. 개념을 암기하기 위한 글이 아니라, 실무에서 병목을 설명할 수 있는 관점을 정리하는 것이 목적입니다.
Socket이란 무엇인가?
소켓은 네트워크 통신을 위한 추상화입니다. 이 추상화는 University of California, Berkeley에서 발전한 BSD 소켓 API에서 출발합니다. 이후 POSIX 표준으로 정리되면서 socket(), bind(), listen(), accept(), connect() 같은 인터페이스가 정의되었습니다.
POSIX 관점에서 중요한 점은 소켓이 file descriptor로 표현된다는 사실입니다. socket() 호출은 정수형 파일 디스크립터를 반환합니다. 이 값은 open()이 반환하는 파일 디스크립터와 동일한 개념입니다.
즉, 운영체제 입장에서는 파일이든 네트워크 소켓이든 동일한 I/O 인터페이스로 다룰 수 있도록 설계되어 있습니다. 이 통합 모델 덕분에 read(), write(), close() 같은 시스템 콜이 파일과 소켓에 동일하게 적용됩니다.
자바의 Socket 역시 내부적으로는 운영체제의 file descriptor를 래핑합니다. SocketInputStream은 결국 네이티브 레벨의 read()를 호출합니다. JVM 위에서 스트림 객체로 보이지만, 실제 데이터 흐름은 커널 네트워크 스택을 통과합니다.
소켓을 파일처럼 취급하는 설계는 우연이 아니라, I/O 모델을 단일화하기 위한 의도적인 결정으로 보입니다. 이 설계를 이해하면 네트워크 문제를 파일 I/O 관점에서 분석할 수 있게 됩니다.
File I/O vs TCP I/O
read()와 write()는 POSIX에서 정의된 기본 I/O 시스템 콜입니다. 파일에 대해 read()를 호출하면 커널은 파일 시스템 캐시에서 데이터를 복사해 사용자 공간 버퍼에 전달합니다. EOF는 더 이상 읽을 데이터가 없음을 의미합니다.
소켓에 대해 read()를 호출하면 동작 방식이 다릅니다. TCP는 스트림 프로토콜이며, RFC 9293에서 정의된 것처럼 바이트 스트림을 제공합니다. TCP는 메시지 경계를 보존하지 않습니다.
파일 I/O는 저장된 데이터를 읽는 것이고, TCP I/O는 네트워크를 통해 도착하는 데이터를 읽는 것입니다. 따라서 소켓 read()는 데이터가 도착하지 않으면 블로킹됩니다. 이것이 blocking semantics입니다.
blocking 모드에서 read()는 최소 1바이트 이상 수신되거나 연결이 종료될 때까지 반환되지 않습니다. non-blocking 모드에서는 즉시 반환되며, 읽을 데이터가 없으면 EAGAIN 같은 오류가 반환됩니다.
파일과 소켓의 가장 큰 차이는 EOF의 의미입니다. 파일에서는 데이터 끝을 의미하지만, TCP에서는 상대방이 FIN을 보냈다는 뜻입니다. 이는 연결 상태 변화이며, 단순한 데이터 부족과는 다릅니다.
이 차이를 이해하지 못하면 스트림이 멈췄을 때 파일 문제인지 네트워크 문제인지 구분하기 어렵습니다.
Buffered I/O의 의미
I/O에는 항상 버퍼가 존재합니다. 사용자 공간 버퍼와 커널 버퍼가 동시에 존재하는 구조입니다.
애플리케이션은 보통 BufferedOutputStream이나 BufferedReader 같은 래퍼를 사용합니다. 이는 user space buffer입니다. 작은 write를 모아서 한 번에 시스템 콜을 호출하기 위한 목적입니다.
커널에도 별도의 소켓 송신 버퍼와 수신 버퍼가 존재합니다. TCP는 내부적으로 세그먼트를 구성하고 재전송을 관리합니다. 이는 RFC 793 및 RFC 9293에서 정의된 동작을 기반으로 합니다.
flush()를 호출하면 사용자 공간 버퍼에서 커널 버퍼로 데이터가 전달됩니다. 그러나 이 시점에 네트워크로 즉시 전송된다고 보장되지는 않습니다. 커널은 혼잡 제어, 윈도우 크기, Nagle 알고리즘 등을 고려합니다.
zero-copy는 이 버퍼 복사를 최소화하기 위한 기법입니다. 대표적으로 sendfile() 시스템 콜이 있습니다. 이는 파일 데이터를 사용자 공간으로 복사하지 않고 커널 공간에서 직접 소켓 버퍼로 전달합니다. 데이터 복사 횟수를 줄이면 CPU 사용량과 메모리 대역폭 사용이 줄어듭니다.
이 구조를 이해하면 대용량 파일 전송에서 왜 zero-copy가 의미 있는지 설명할 수 있습니다.
packet, segment, frame의 차이
이 용어들은 OSI 모델과 TCP/IP 모델에서 정의된 계층 개념과 연결됩니다. OSI 기본 참조 모델은 ISO/IEC 7498-1에 정의되어 있습니다.
Frame은 Data Link 계층 단위입니다. 물리적 네트워크에서 전송되는 실제 프레임 구조입니다.
Packet은 IP 계층 단위입니다. RFC 791에서 정의된 IP 패킷은 논리적 주소와 라우팅 정보를 포함합니다.
Segment는 TCP 계층 단위입니다. RFC 9293에서 정의된 TCP 세그먼트는 포트 번호, 시퀀스 번호 등을 포함합니다.
데이터는 애플리케이션에서 내려가며 segment가 되고, 그 위에 IP 헤더가 붙어 packet이 되며, 다시 Data Link 헤더가 붙어 frame이 됩니다. 이를 encapsulation이라고 부릅니다.
자바 코드에서는 이 단계를 직접 볼 수 없습니다. 우리는 스트림 단위로만 데이터를 다룹니다. 그러나 네트워크 분석 도구에서 패킷을 보면 이 계층 구조가 그대로 드러납니다.
실무에서 발생하는 병목
TCP는 작은 데이터를 즉시 전송하지 않을 수 있습니다. Nagle 알고리즘은 작은 패킷을 모아서 전송하기 위한 최적화입니다. 이 개념은 RFC 896에서 논의되었습니다.
Delayed ACK 역시 수신 측이 ACK를 즉시 보내지 않고 지연시키는 전략입니다. 이 둘이 결합되면 작은 write가 지연되는 현상이 발생할 수 있습니다.
MTU는 한 번에 전송 가능한 최대 프레임 크기입니다. 이를 초과하면 fragmentation이 발생합니다. IP 단편화는 RFC 791에서 정의되어 있습니다.
또한 모든 read()와 write()는 시스템 콜입니다. syscall은 사용자 모드와 커널 모드 전환을 수반합니다. 작은 단위로 자주 호출하면 context switching 비용이 증가합니다.
이러한 요소들은 코드 레벨에서는 보이지 않지만, 성능 문제로 나타납니다.
LLM 스트리밍 응답에서의 실제 흐름
HTTP/1.1에서는 chunked transfer encoding이 정의되어 있습니다. 이는 RFC 7230에 명시되어 있습니다. 서버는 길이를 모르는 응답을 chunk 단위로 전송할 수 있습니다.
HTTP/2는 프레임 기반 프로토콜이며 RFC 7540에 정의되어 있습니다. HTTP 메시지는 binary framing layer 위에서 전송됩니다.
최근에는 QUIC 기반 HTTP/3도 사용됩니다. 이는 RFC 9000에서 정의된 전송 프로토콜 위에서 동작합니다.
LLM 스트리밍 응답은 애플리케이션 레벨에서 chunk를 생성하고, 이를 TCP 스트림으로 write합니다. 그러나 토큰이 생성되는 즉시 사용자에게 보이는 것은 아닙니다. 사용자 공간 버퍼, 커널 버퍼, 혼잡 제어, ACK 전략 등이 영향을 줍니다.
flush() 전략을 어떻게 설계하느냐에 따라 체감 지연은 달라질 수 있습니다. 이 흐름을 이해하면 스트리밍이 “느리게 느껴지는” 이유를 설명할 수 있습니다.
마무리하며
소켓은 파일 디스크립터입니다. TCP는 바이트 스트림입니다. 그리고 우리가 다루는 스트림은 여러 계층의 캡슐화를 거쳐 전달됩니다.
LLM 시대에도 네트워크 I/O를 이해하는 일은 여전히 중요합니다. API 호출이 실패했을 때, 지연이 발생했을 때, 스트리밍이 끊겼을 때 어디를 봐야 하는지 판단하기 위해서입니다.
이 글을 정리하면서 느낀 점은, 소켓을 파일처럼 다루는 운영체제의 설계가 얼마나 강력한 추상화인지 다시 생각하게 되었다는 점입니다. 기본 모델을 이해하는 것이 복잡한 시스템을 다루는 데 여전히 유효하다고 생각합니다.
'STUDY' 카테고리의 다른 글
- Total
- Today
- Yesterday
- InterruptedException
- 캐시와 인덱스
- Redis 캐시 전략
- 백엔드 아키텍처
- Eager Initialization
- 동시성처리
- Redis vs DB
- 백엔드 성능 설계
- 백엔드 성능
- TTL 설계
- 백엔드 성능 튜닝
- DB 인덱스 성능
- mybatis
- spring batch 5
- Cache Aside
- Double-Checked Locking
- Java Performance
- Cache Avalanche
- Hot Key 문제
- Cache Penetration
- Spring Batch
- 트래픽 처리
- 스레드 생명주기
- 트랜잭션 관리
- Redis 성능 개선
- DB 트랜잭션
- 캐시 장애
- 캐시 성능 비교
- Initialization-on-Demand Holder Idiom
- Enum 기반 싱글톤
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |
| 29 | 30 | 31 |

