TCP는 신뢰성 있는 프로토콜이라고 배운다. 정확히는 세 가지를 보장한다.
순서 보장
세그먼트가 도착 순서와 다르게 뒤섞여 와도 수신 측이 시퀀스 번호를 보고 원래 순서로 재조립한다.
완전성 보장
세그먼트가 유실되면 재전송을 요구해서 결국 모든 데이터가 도착한다.
중복 제거
같은 데이터가 두 번 도착해도 시퀀스 번호로 걸러낸다.
이 세 가지를 묶어서 신뢰성 있는 전송이라고 부른다.
순서, 완전성, 중복 제거 이 세 가지 안에 "내가 보낸 단위 그대로 도착한다"는 항목은 없다.
애플리케이션이 write()를 세 번 호출해서 데이터를 세 덩어리로 보냈다고 해보자. 수신 측이 read()를 세 번 호출하면 그 세 덩어리가 그대로 나뉘어 들어올까?
그렇지 않다. 두 덩어리로 합쳐져 올 수도 있고, 반대로 한 덩어리가 두 번에 나뉘어 올 수도 있다.
TCP는 바이트 스트림을 보장하는 프로토콜이다. 순서가 맞고 빠짐없이 도착한 연속된 바이트의 흐름을 보장할 뿐, 그 흐름 안에서 애플리케이션이 정한 경계를 지켜주지는 않는다.
애플리케이션은 네트워크에 직접 쓰지 않는다. write()를 호출하면 데이터는 커널이 관리하는 송신 버퍼에 들어갈 뿐이다.
TCP는 이 버퍼에 쌓인 데이터를 알아서 잘라 세그먼트로 내보낸다. 그 크기는 MSS(Maximum Segment Size)로 정해진다. 애플리케이션이 한 번에 얼마를 보냈는지와는 무관하다.
애플리케이션이 MSS보다 큰 데이터를 한 번에 보내면 여러 세그먼트로 쪼개진다. 반대로 MSS보다 훨씬 작은 데이터를 여러 번 보내면 버퍼에 쌓였다가 한 세그먼트에 같이 담길 수 있다.
세그먼트의 경계를 정하는 건 애플리케이션의 호출 단위가 아니다. 송신 버퍼와 MSS다.
가장 분명한 사례가 Nagle 알고리즘이다. Nagle 알고리즘은 송신 최적화(전송 스케줄링)기법 이라고한다. 이 알고리즘은 아직 나가지 않은 데이터를 언제 내보낼지 결정한다.
이 알고리즘의 규칙은 단순하다. 아직 확인응답(ACK)을 받지 못한 데이터가 남아있는 동안에는, 새로 들어온 작은 데이터를 곧바로 내보내지 않는다. 다음 둘 중 하나가 될 때까지 송신 버퍼에 쌓아둔다.
쌓인 데이터가 MSS 크기를 채웠을 때 또는 이전에 보낸 데이터에 대한 ACK가 돌아왔을 때다.
그러니까 이 알고리즘은 무작정 기다리는 게 아니다. 지금 나가는 데이터에 대한 응답을 아직 못 받았다면 그 사이에 들어오는 작은 조각들은 다음 기회에 묶어서 보내자는 조건부 대기다. 이렇게 하면 짧은 시간에 여러 번 나갈 뻔한 작은 세그먼트가, 이전 전송의 응답을 기다리는 동안 자연스럽게 하나로 뭉쳐 나간다.
생각해보면 이 알고리즘의 존재 자체가 증거다. TCP에 "애플리케이션이 보낸 단위를 그대로 보존한다"는 원칙이 있었다면, 애초에 여러 번의 전송을 하나로 합치는 알고리즘이 나올 이유가 없다.
TCP는 처음부터 전송 단위를 재구성할 수 있다는 전제 위에서 설계됐다.
송신 측만 보면 이 이야기가 반쪽처럼 느껴질 수 있다. 수신 측에서도 같은 일이 벌어진다.

세그먼트는 도착 순서가 뒤섞여 올 수 있다. TCP는 시퀀스 번호를 보고 이걸 원래 순서로 재정렬한 다음, 순서가 맞는 것부터 수신 버퍼에 쌓는다. 이 버퍼는 세그먼트 단위로 나뉘어 있지 않다. 그냥 이어진 바이트의 큐다.
애플리케이션이 read()를 호출하면 커널은 이 큐에서 지금 있는 바이트 중 요청한 만큼만 꺼내 돌려준다. 큐에 요청한 크기보다 적게 쌓여 있으면 그만큼만 돌아온다. 반대로 여러 세그먼트에 걸쳐 쌓인 데이터라도 요청한 크기를 채울 만큼 있으면 한 번에 다 돌아올 수 있다.
이 지점에서 "몇 개의 세그먼트로 도착했는가"라는 정보는 완전히 사라진다. read() 결과만 봐서는 이게 세그먼트 하나에서 온 건지, 세 개가 합쳐진 건지 구분할 방법이 없다.
그래서 TCP 소켓 프로그래밍에서는 한 번의 read()로 원하는 만큼 다 받는다고 가정하면 안 된다는 규칙이 나온다. 필요한 크기를 채울 때까지 read()를 반복 호출하는 패턴이 흔한 이유가 여기 있다.
TCP는 순서대로, 빠짐없이, 중복 없이 전달한다. 다만 내가 보낸 모양 그대로 전달한다는 약속은 없다.
write()를 몇 번 나눠 불렀는지, read()가 몇 번 만에 그걸 받아가는지, 소켓 버퍼 입장에서는 애초에 상관없는 일이다. Nagle 알고리즘이 작은 데이터를 묶어 보내는 것도 결국 TCP가 애플리케이션의 호출 단위를 보존하지 않는다는 사실에서 나온다.