9.10. TCP – 신뢰성 있는 바이트 스트림#

TCP, 즉 전송 제어 프로토콜(Transmission Control Protocol)은 IP 위에서 동작하는 또 다른 전송 계층 서비스입니다. UDP를 “패킷을 보내고 잘 도착하기를 바라는” 방식이라고 표현한다면, TCP는 “두 종단점 사이에 연결을 열고 이를 양방향 바이트 파이프로 다루며, 상대방이 그 바이트를 반드시, 순서대로, 정확히 한 번 받도록 보장하는” 방식입니다. 대부분의 인터넷 트래픽이 TCP를 사용하며, 카메라가 네트워크에서 하는 일의 대부분도 TCP를 사용합니다.

9.10.1. TCP가 IP에 더하는 것#

TCP는 UDP보다 훨씬 많은 일을 합니다. TCP는 다음을 추가합니다:

  • 연결. 데이터가 흐르기 전에, 두 종단점은 짧은 핸드셰이크를 주고받아 서로 통신하고 있음을 합의합니다. 연결에는 “열림”, “반-닫힘”, “닫힘”이라는 상태가 있으며, 양쪽 모두 이를 추적합니다.

  • 신뢰성 있는 전달. 송신자가 넣은 모든 바이트는 수신자가 확인 응답합니다. 타임아웃 내에 확인 응답되지 않은 것은 다시 전송됩니다. 애플리케이션은 손실된 바이트를 결코 보지 못하며, 프로토콜이 재전송하는 동안의 지연만을 보게 됩니다.

  • 순서대로 전달. 바이트는 보낸 순서 그대로 도착합니다. 설령 패킷이 수신 측에 순서가 뒤바뀐 채 도착하더라도, TCP는 애플리케이션이 읽기 전에 이를 다시 정렬합니다.

  • 흐름 제어. 수신자가 느리면 송신자에게 속도를 늦추라고 알립니다. 연결은 더 느린 쪽의 속도에 맞춰 적응합니다.

  • 혼잡 제어. 중간 네트워크가 패킷을 버리기 시작하면, 송신자는 상황이 회복될 때까지 전송량을 줄입니다. 이는 어느 한 연결이 포화된 링크를 마비시키는 것을 막아 줍니다.

이 모든 것은 자동으로 이루어집니다. 애플리케이션이 사용하는 Python API는 단순히 send(bytes)recv(n)뿐이며, 나머지는 모두 TCP가 그 아래에서 처리합니다.

9.10.2. 핸드셰이크#

TCP 연결은 데이터가 통과하기 전에 세 단계 교환으로 열립니다:

A diagram with two columns labelled "client" and "server". An arrow from client to server labelled "SYN", then an arrow from server to client labelled "SYN-ACK", then an arrow from client to server labelled "ACK". Below that a thick arrow labelled "data flowing both ways".

3단계 핸드셰이크. 양쪽이 모두 확인 응답하고 나면 연결이 열리고 데이터가 흐를 수 있습니다.#

클라이언트가 연결을 열어달라는 SYN(동기화) 패킷을 보냅니다. 서버는 SYN-ACK(동기화 + 확인 응답)로 응답하며 이를 수락합니다. 클라이언트는 마지막으로 ACK를 보내 확정합니다. 이 왕복 이후 양쪽은 연결이 열렸다는 데 합의하며, 어떤 바이트가 송수신되었는지 추적하기 위한 카운터를 동기화합니다.

핸드셰이크는 첫 유효 바이트가 통과하기 전에 1회 왕복 시간(round-trip-time)만큼의 지연 시간을 소모합니다. 로컬 네트워크에서는 1밀리초 정도이지만, 대륙을 가로지르는 연결에서는 대략 100ms입니다. 이것이 TCP의 주요 비용이며, 짧은 단발성 메시지가 때로는 UDP를 쓰는 편이 나은 이유입니다.

9.10.2.1. 종료 핸드셰이크#

TCP 연결은 종료할 때도 교환을 거칩니다(각 측에서 보내는 FIN). 어느 쪽이든 자신의 연결 절반을 독립적으로 닫을 수 있습니다. 한쪽은 송신을 끝냈지만 다른 쪽은 여전히 통신 중인 반-닫힘 상태도 적법하지만, 흔하지는 않습니다. 애플리케이션은 보통 close()를 호출하기만 하고, 종료 시퀀스는 프로토콜에 맡깁니다.

9.10.3. TCP가 보장하지 않는#

TCP가 한다고 종종 가정되지만 실제로는 하지 않는 몇 가지:

  • 메시지 경계. 애플리케이션은 메시지 스트림이 아니라 바이트 스트림을 보냅니다. 두 번의 send(b"hello") 호출이 b"hellohello"라는 하나의 recv()로 도착할 수도 있고, 크기가 제각각인 두 번의 recv()로 도착할 수도 있습니다. 애플리케이션이 메시지 프레이밍을 원한다면 직접 프레이밍을 추가해야 합니다(개행 문자든, 길이 접두어든, 무엇이든). 예를 들어 TCP로 JSON 문서를 보낼 때는 각 문서를 개행 문자나 다른 구분자로 분리해야 합니다.

  • 암호화. TCP는 애플리케이션이 준 바이트를 끝까지 평문 그대로 운반합니다. 내용이 기밀이어야 한다면, 애플리케이션이 연결을 TLS로 감싸야 합니다(암호화된 소켓과 TLS 참조).

  • 인증. TCP는 바이트가 손상 없이 도착했는지 확인합니다. 하지만 누가 보냈는지에 대해서는 아무것도 말해 주지 않습니다. 인증 역시 상위 계층의 관심사입니다.

  • 조용한 연결에서의 생존성. 양쪽이 오랫동안 데이터를 보내지 않으면, 연결은 기술적으로는 여전히 열려 있지만 상대방이 충돌했거나 사라졌음을 감지하지 못합니다. 이것이 중요한 경우에는 소켓에서 Keepalive 탐색을 활성화하여 해결할 수 있습니다.

9.10.4. TCP를 사용할 때#

TCP는 “클라이언트가 서버에 연결을 열고, 약간의 바이트를 주고받은 뒤, 끝나면 연결을 닫는” 형태에 맞는 거의 모든 대화에 적합한 답입니다. HTTP 및 HTTPS 요청, SSH 세션, 데이터베이스 쿼리, 파일 전송, 이미지 업로드 – 모두 TCP 위에서 이루어집니다.

대화가 그 형태에 맞지 않을 때만 UDP를 사용하세요: 손실을 허용할 수 있는 독립적이고 자기 완결적인 메시지, 멀티캐스트 트래픽, 시간 동기화, 이름 조회, 또는 핸드셰이크 비용을 감당할 수 없는 극도로 지연에 민감한 경우입니다.

포트, UDP, TCP가 모두 갖춰지면서 전송 계층 이야기는 마무리되었습니다. 이 둘을 모두 노출하는 Python API는 다음 페이지에 있습니다: 소켓 객체.