![[Network] reliable data transfer](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FwN1Jt%2FbtsNtheF1G9%2FAAAAAAAAAAAAAAAAAAAAALHKHsm6pEvmP8jvnDKk_oVT2Bw2aOLcSqeaa8uDgl0i%2Fimg.jpg%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1756652399%26allow_ip%3D%26allow_referer%3D%26signature%3D9b2%252FuoOqPWsBrnap%252FLNqHg%252BiR8I%253D)
신뢰성 있는 데이터 전송 문제는 네트워킹에서 중요한 주제이다. 이 문제가 전송 계층 뿐만 아니라 링크 계층, 애플리케이션 계층에서도 발생할 수 있는 문제이기 때문이다. 네트워킹에서 가장 중요한 문제들 중 Top 10을 만든다면, 이 문제가 최상위에 위치할 후보일 것이다.
이후 TCP를 살펴볼 때, TCP가 이 섹션에서 설명하는 원칙들을 어떻게 활용하는지 확인할 수 있을 것이다. TCP를 위한 빌드업이라고 보면 될 것 같다.
애플리케이션과 같은 상위 계층이 원하는 것은 간단하다. "내 데이터를 정확하게, 손실 없이, 순서대로 상대방에게 전달해 달라"는 것이다. 이것이 바로 '신뢰성 있는 채널'이 제공하는 서비스다.
하지만 실제 네트워크 환경은 완벽하지 않다. 데이터 전송 중에 비트가 손상되거나, 패킷이 완전히 사라지거나, 순서가 뒤바뀔 수 있다. 이런 불완전한 환경에서도 마치 완벽한 채널처럼 동작하도록 만드는 것이 '신뢰성 있는 데이터 전송 프로토콜'의 역할이다.
당연하게도 이 다음 절에서 다룰 것이 신뢰적인 데이터 전송 원리가 적용된 TCP다. TCP는 패킷 손실이나 오류가 발생할 수 있는 IP 네트워크 위에서 동작하면서도, 웹 브라우저나 이메일 앱에게는 안정적인 데이터 전송을 보장한다.
이 원리들을 이야기하면서 가지고 있을 한 가지 가정은 패킷이 보낸 순서대로 전달되고(일부 패킷은 손실될 수 있음), 기본 채널이 패킷 순서를 재정렬하지 않는다는 것이다.
이 섹션에서는 '단방향 데이터 전송'만 다룬다. 즉, 실제 정보(파일, 메시지 등)는 송신자에서 수신자 방향으로만 흐른다고 가정한다. 하지만 '데이터'는 한 방향으로만 전송되더라도, '제어 메시지'는 양방향으로 오간다는 것이다. 데이터 패킷이 제대로 도착했는지 확인하는 응답(ACK), 손상된 패킷을 알리는 부정 응답(NAK) 등의 제어 메시지는 수신자에서 송신자로 전송되어야 하기 때문이다. 따라서 rdt 프로토콜은 실제로는 양쪽 모두 패킷을 보내고 받을 수 있어야 한다.
📡 rdt 1.0
rdt 1.0은 가장 단순한 상황을 가정한다. 기본 채널이 완벽하게 신뢰할 수 있는 경우다. 다시 말해, 비트 오류, 패킷 손실, 순서 변경이 전혀 발생하지 않는 이상적인 상황이다.
송신자와 수신자 모두 단 하나의 상태만 가진 매우 단순한 FSM으로 표현된다. 왜냐하면 아무런 문제가 발생하지 않는다고 가정하기 때문이다.
송신자의 동작
- 상위 계층에서 데이터 전송 요청이 오면(`rdt_send(data)`)
- 데이터를 포함한 패킷을 생성하고(`make_pkt(data)`),
- 그 패킷을 채널로 보낸다(`udt_send(packet)`).
수신자의 동작
- 하위 채널에서 패킷을 받으면(`rdt_rcv(packet)`)
- 패킷에서 데이터를 추출하고(`extract(packet, data)`),
- 그 데이터를 상위 계층에 전달한다(`deliver_data(data)`).
이 때 완벽한 채널을 가정하므로 수신자가 송신자에게 어떤 피드백도 보낼 필요가 없다. 데이터와 패킷 사이에 차이가 없고, 모든 패킷은 송신자에서 수신자로만 흐른다. 이 때, 수신자는 송신자가 보내는 속도를 따라갈 수 있다고 가정한다.
rdt 1.0은 실제로는 사용할 수 없는 이상적인 모델이지만 더 복잡한 프로토콜을 이해하기 위한 기본 출발점으로서 의의가 있다. 이후 rdt 버전들은 실제 네트워크에서 발생하는 다양한 문제(비트 오류, 패킷 손실)를 다루기 위해 점진적으로 더 복잡해진다.
📡 rdt 2.0
1.0보다 더 현실적인 채널 모델은 어떤 형태가 있을까? 바로 패킷의 비트가 손상되는 경우다. 이런 비트 오류는 패킷이 전송, 전파 또는 버퍼링될 때 네트워크의 물리적 구성 요소에서 주로 발생한다. 여전히 모든 패킷은 보낸 순서대로 수신된다고 가정하지만 비트는 손상될 수 있다.
예를 들어 전화로 메시지를 받아쓰는 상황을 생각해보자. 일반적으로 받아쓰는 사람은 문장을 듣고 이해한 후 "OK"라고 응답한다. 만약 잘 안들리면 "다시 말해주세요"라고 요청할 것이다. 이 과정은 긍정 응답과 부정 응답을 모두 사용한다. 이렇게 해서 수신자는 무엇이 올바르게 수신되었는지, 아니면 오류로 수신되어 반복이 필요한지 송신자에게 알릴 수 있다.
컴퓨터 네트워크에서 이러한 재전송 기반 프로토콜을 ARQ$_{Automatic \space Repeat \space reQuest}$(자동 재전송 요구) 프로토콜이라고 한다.
비트 오류를 처리하기 위해 ARQ 프로토콜에는 세 가지 추가 기능이 필요하다.
- 오류 검출 수신자가 비트 오류를 감지할 수 있는 메커니즘이 필요하다. 체크섬 필드가 이 목적으로 사용된다.
- 수신자 피드백 송신자와 수신자는 일반적으로 서로 다른 시스템에서 실행되므로 수신자의 상황(패킷이 올바르게 수신되었는지 여부)을 송신자가 알 수 있는 유일한 방법은 수신자가 명시적인 피드백을 제공하는 것이다. 예시로 ACK(긍정 응답)과 NAK(부정 응답) 메시지가 있다.
- 재전송 수신자가 오류로 수신한 패킷은 송신자가 재전송한다.
그래서 rdt 2.0는 오류 감지, 긍정 응답(ACK), 부정 응답(NAK) 세 가지를 채택해 사용한다.
송신자 측 동작
- 송신자는 두 가지 상태를 가진다.
- 첫 번째 상태에서는 상위 계층으로부터 데이터를 기다린다.
- 데이터가 도착하면 체크섬과 함께 패킷을 만들어(`make_pkt(data, checksum)`) 전송(`udt_send(sndpkt)`)한다.
- 두 번째 상태에서는 수신자로부터 ACK 또는 NAK을 기다린다.
- ACK를 받으면 다시 첫 번째 상태로 돌아가 새 데이터를 기다린다.
- NAK을 받으면 마지막 패킷을 재전송(`udt_send(sndpkt)`)하고 ACK/NAK을 기다린다.
- 첫 번째 상태에서는 상위 계층으로부터 데이터를 기다린다.
수신자 측 동작
- 수신자는 하나의 상태만 가진다.
- 패킷이 도착(`rdt_rcv(rcvpkt)`)하면 손상 여부를 확인(`corrupt(rcvpkt)`)한다.
- 패킷이 올바르면(`notcurrupt(rcvpkt)`) ACK을 보낸다.
- 패킷이 손상되었으면 NAK을 보낸다(`udt_send(NAK)`).
하지만 rdt 2.0에는 심각한 문제가 있다. rdt 2.0에서는 ACK나 NAK 패킷 자체가 손상될 수 있다는 가능성을 고려하지 않았다. 이 경우 송신자는 수신자가 마지막 데이터를 올바르게 받았는지 여부를 알 수 없게 된다.
📡 rdt 2.1, 2.2
이 문제를 해결하기 위해 rdt 2.1에서는 sequence number(시퀀스 번호)라는 새로운 필드를 도입한다. 송신자는 데이터 패킷에 번호를 부여하고, 수신자는 이 번호를 확인해 패킷이 재전송인지 새 패킷인지 판단할 수 있다. 전송 후 대기(stop-and-wait) 프로토콜에서는 1비트 시퀀스 번호(0 또는 1)로 충분하다.
rdt 2.2는 NAK을 사용하지 않고 ACK만 사용하는 방식으로 더 발전했다. 손상된 패킷을 받으면 수신자는 NAK을 보내는 대신, 마지막으로 올바르게 수신한 패킷에 대한 ACK를 보낸다. 송신자가 동일한 패킷에 대해 ACK를 두 번 받으면(중복 ACK), 그 이후 패킷이 올바르게 수신되지 않았다고 판단할 수 있다.
이러한 방식으로 RDT 프로토콜은 비트 오류가 있는 채널에서도 신뢰성 있는 데이터 전송을 제공할 수 있게 된다.
📡 rdt 3.0
지금까지 살펴본 채널은 비트를 손상시킬 수 있었지만, 패킷 자체가 손실되는 경우는 고려하지 않았다. 그러나 현실에서의 컴퓨터 네트워크에서는 당연하게도 패킷 손실이 흔하게 발생한다. 따라서 프로토콜은 패킷 손실을 어떻게 감지하고, 패킷 손실이 발생했을 때 어떻게 대처할 지에 대한 두 가지 문제를 추가적으로 해결해야 한다.
패킷 손실 감지 및 복구 방법
패킷 손실 문제를 해결하기 위한 여러 접근법이 있지만, rdt 3.0에서는 송신자에게 패킷 손실 감지와 복구의 책임을 부여한다.
- 송신자가 데이터 패킷을 전송한다.
- 해당 패킷이나 수신자의 ACK가 손실되면, 송신자는 수신자로부터 아무런 응답을 받지 못한다.
- 일정 시간 기다린 후에도 응답이 없으면, 송신자는 패킷이 손실되었다고 판단하고 패킷을 재전송한다.
타이머의 도입
핵심 질문은 "송신자가 얼마나 오래 기다려야 하는가?"다. 이론적으로는 송신자와 수신자 간의 왕복 지연 시간(RTT) 이상을 기다려야 한다. 하지만 많은 네트워크에서 최악의 경우 지연 시간을 정확히 예측하기 어렵다.
실제로는 적절한 타임아웃 값을 선택해 그 시간 내에 ACK가 도착하지 않으면 패킷 손실이 발생했다고 가정하고 재전송한다. 이 접근법의 단점은 패킷이 단순 지연되었을 경우에도 재전송될 수 있어 중복 패킷이 발생할 수 있다는 점이다. 다행히 rdt 2.2에서 도입한 sequence number가 중복 패킷 문제를 처리할 수 있다.
타이머 기반 재전송 메커니즘을 구현하려면 송신자는 아래와 같은 기능이 필요하다.
- 패킷을 보낼 때마다 타이머 시작
- 타이머 인터럽트에 대응(적절한 조치 수행)
- 타이머 중지
rdt 3.0은 비트 오류와 패킷 손실이 모두 발생할 수 있는 채널에서 데이터를 안정적으로 전송하는 프로토콜이다.
송신자 측 동작
- 송신자는 4가지 상태를 가진다.
- Wait for call 0 from above (상위 계층으로부터 호출 0 대기)
- 데이터가 도착하면(`rdt_send(data)`) 시퀀스 번호 0, 데이터, 체크섬을 포함한 패킷을 만들어(`make_pkt(0, data, checksum)`) 전송하고(`udt_send(sndpkt)`) 타이머를 시작(`start_timer`)한다.
- ‘ACK 0 대기 상태’로 전환한다.
- Wait for ACK 0 (ACK 0 대기)
- 올바른 ACK 0을 받으면(`rdt_rcv(rcvpkt) && notcorrupt(rcvpkt) && isACK(rcvpkt, 0)`) 타이머를 중지(`stop_timer`)하고 ‘상위 계층으로부터 호출 1 대기’ 상태로 전환한다.
- 손상된 ACK나 ACK 1을 받으면(`rdt_rcv(rcvpkt) && (corrupt(rcvpkt) || isACK(rcvpkt, 1))`) 무시하고 계속 기다린다.
- 타임아웃이 발생하면(`timeout`) 마지막 패킷을 재전송하고 타이머를 다시 시작한다.
- Wait for call 1 from above (상위 계층으로부터 호출 1 대기)
- 데이터가 도착하면(`rdt_send(data)`) 시퀀스 번호 1, 데이터, 체크섬을 포함한 패킷을 만들어(`make_pkt(1, data, checksum)`) 전송하고(`udt_send(sndpkt)`) 타이머를 시작한다.
- ‘ACK 1 대기’ 상태로 전환한다.
- Wait for ACK 1 (ACK 1 대기)
- 올바른 ACK 1을 받으면(`rdt_rcv(rcvpkt) && notcorrupt(rcvpkt) && isACK(rcvpkt, 1)`) 타이머를 중지하고 ‘상위 계층으로부터 호출 0 대기’ 상태로 전환한다.
- 손상된 ACK나 ACK 0을 받으면(`rdt_rcv(rcvpkt) && (corrupt(rcvpkt) || isACK(rcvpkt, 0))`) 무시하고 계속 기다린다.
- 타임아웃이 발생하면(`timeout`) 마지막 패킷을 재전송하고 타이머를 다시 시작한다.
- Wait for call 0 from above (상위 계층으로부터 호출 0 대기)
수신자 측 동작
- 수신자는 이전 버전과 마찬가지로 올바른 패킷에 ACK를 보내고, 손상된 패킷은 무시한다.
패킷 시퀀스 번호가 0과 1 사이에서 번갈아 가기 때문에 rdt 3.0은 alternating-bit protocol이라고도 한다.
마무리
여기까지 살펴본 rdt 프로토콜은 TCP를 어떻게 구성할지 완성하기 위한 조각들을 맞춰가는 과정이었다. 완벽한 채널을 가정한 rdt 1.0, 비트 오류를 다룬 2.x, 패킷 손실까지 고려한 rdt 3.0까지 각 단계는 실제 상황에서의 네트워크가 얼마나 복잡하고 예측 불가능한지를 보여주면서도 그 안에서도 어떻게 신뢰성을 만들어낼 수 있는지를 설명해줬다.
결국 rdt를 통해 하고 싶었던 것은 상대방에게 데이터를 정확하게, 빠짐없이, 순서대로 보내고 싶은 하나만의 목표를 가지고 발전했다. 당연하지만 절대로 당연하지 않은 요구를 만족시키기 위해 수많은 고민과 기법들이 쌓여왔다. rdt는 그 첫걸음이고 다음에 살펴보게 될 TCP는 그 고민이 쌓여서 만들어낸 결과물이다.
rdt를 이해했다면, TCP도 분명 덜 낯설게 느껴질 것이다. 왜 ACK를 쓰는지, 타이머가 필요한지, sequence number가 중요한지에 대한 답이 이미 다 있었다.
references
Computer Networking: A Top-Down Approach, 8th edition
COMP0414 수업 내용
'CSE > 네트워크 (network)' 카테고리의 다른 글
[Network] HTTP/1.0, HTTP/1.1, HTTP/2.0, HTTP/3.0 (0) | 2025.04.22 |
---|---|
[Network] Go-Back-N, Selective Repeat (0) | 2025.04.20 |
[Network] UDP (0) | 2025.04.20 |
[Network] multiplexing, demultiplexing (0) | 2025.04.20 |
[Network] Fairness (0) | 2025.04.20 |
컴퓨터 전공 관련, 프론트엔드 개발 지식들을 공유합니다. React, Javascript를 다룰 줄 알며 요즘에는 Typescript에도 관심이 생겨 공부하고 있습니다. 서로 소통하면서 프로젝트 하는 것을 즐기며 많은 대외활동으로 개발 능력과 소프트 스킬을 다듬어나가고 있습니다.
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!