본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 12. 5. · 13 Views
TCP 오류/흐름/혼잡 제어 완벽 가이드
TCP 통신에서 데이터가 안전하게 전달되도록 보장하는 핵심 메커니즘들을 알아봅니다. 오류 제어, 흐름 제어, 혼잡 제어의 원리와 실제 구현 방식을 초급 개발자도 이해할 수 있도록 쉽게 설명합니다.
목차
- 오류 제어와 재전송 기법
- Stop-and-Wait ARQ
- Go-Back-N ARQ
- Selective Repeat ARQ
- 흐름 제어: 슬라이딩 윈도우
- 혼잡 제어 알고리즘
- ECN(명시적 혼잡 알림)
1. 오류 제어와 재전송 기법
김개발 씨는 회사에서 파일 전송 기능을 구현하던 중 이상한 현상을 발견했습니다. 분명히 100MB 파일을 보냈는데, 받은 쪽에서는 파일이 깨져 있다는 겁니다.
"인터넷으로 데이터를 보내면 그냥 도착하는 거 아니었어?" 김개발 씨의 머릿속에 물음표가 가득 찼습니다.
오류 제어란 네트워크 통신 중 발생하는 데이터 손실이나 손상을 감지하고 복구하는 메커니즘입니다. 마치 중요한 서류를 등기우편으로 보내고 수신 확인을 받는 것과 같습니다.
TCP는 신뢰성 있는 통신을 보장하기 위해 **ACK(확인응답)**과 재전송이라는 두 가지 핵심 도구를 사용합니다.
다음 코드를 살펴봅시다.
import socket
import time
class ReliableSender:
def __init__(self, timeout=2.0):
self.timeout = timeout # 재전송 타임아웃 설정
self.seq_num = 0 # 시퀀스 번호 추적
def send_with_retry(self, data, max_retries=3):
# 재전송 로직 구현
for attempt in range(max_retries):
packet = self._create_packet(self.seq_num, data)
self._send(packet)
# ACK 대기
if self._wait_for_ack(self.seq_num):
self.seq_num += 1 # 성공시 다음 시퀀스로
return True
print(f"타임아웃 발생, 재전송 시도 {attempt + 1}")
return False # 최대 재시도 초과
김개발 씨는 입사 6개월 차 백엔드 개발자입니다. 오늘 그에게 주어진 임무는 대용량 파일 전송 기능을 구현하는 것이었습니다.
"그냥 소켓으로 보내면 되는 거 아닌가?" 처음에는 단순하게 생각했습니다. 그런데 테스트 중 문제가 발생했습니다.
100MB 파일을 전송했는데, 받은 쪽에서 열어보니 파일이 손상되어 있었습니다. 당황한 김개발 씨에게 선배 박시니어 씨가 다가왔습니다.
"네트워크는 생각보다 불안정해요. 패킷이 중간에 사라지기도 하고, 순서가 뒤바뀌기도 하거든요." 박시니어 씨의 설명에 김개발 씨는 고개를 갸웃거렸습니다.
"그럼 어떻게 해야 하나요?" 오류 제어를 이해하려면 먼저 네트워크의 본질을 알아야 합니다. 인터넷은 마치 수천 개의 우체국을 거쳐 편지가 전달되는 것과 같습니다.
그 과정에서 편지가 분실되거나, 찢어지거나, 순서가 뒤바뀔 수 있습니다. 그래서 TCP는 특별한 방법을 사용합니다.
바로 **확인응답(ACK)**입니다. 데이터를 보낸 후 상대방이 "잘 받았습니다"라고 응답해주는 것입니다.
마치 등기우편의 수신 확인 도장과 같습니다. 만약 일정 시간 내에 ACK가 도착하지 않으면 어떻게 될까요?
송신자는 "아, 뭔가 문제가 생겼구나"라고 판단하고 같은 데이터를 다시 보냅니다. 이것이 바로 **재전송(Retransmission)**입니다.
여기서 중요한 개념이 타임아웃입니다. 얼마나 기다려야 "문제가 생겼다"고 판단할 수 있을까요?
너무 짧으면 정상적인 지연도 오류로 오해할 수 있고, 너무 길면 실제 오류 상황에서 복구가 늦어집니다. TCP는 이 문제를 해결하기 위해 적응형 타임아웃을 사용합니다.
네트워크 상태를 지속적으로 모니터링하여 적절한 대기 시간을 동적으로 조절합니다. 빠른 네트워크에서는 짧게, 느린 네트워크에서는 길게 설정합니다.
위 코드를 살펴보면, send_with_retry 함수가 핵심입니다. 데이터를 보내고 ACK를 기다리며, 타임아웃이 발생하면 최대 3번까지 재전송을 시도합니다.
시퀀스 번호를 통해 어떤 패킷에 대한 ACK인지 구분합니다. 실무에서 이런 로직은 TCP 계층에서 자동으로 처리됩니다.
하지만 UDP를 사용하거나 애플리케이션 레벨에서 추가적인 신뢰성이 필요할 때는 직접 구현해야 할 수도 있습니다. 김개발 씨는 고개를 끄덕였습니다.
"그래서 TCP가 신뢰성 있는 프로토콜이라고 하는 거군요!" 이제 파일이 왜 깨졌는지, 그리고 어떻게 해결해야 하는지 감이 잡히기 시작했습니다.
실전 팁
💡 - 타임아웃 값은 네트워크 RTT(왕복 시간)를 기반으로 동적 설정하는 것이 좋습니다
- 재전송 횟수에 제한을 두어 무한 루프를 방지하세요
2. Stop-and-Wait ARQ
김개발 씨는 오류 제어의 기본 개념을 이해했지만, 구체적으로 어떻게 구현되는지 궁금해졌습니다. 박시니어 씨가 화이트보드 앞으로 그를 데려갔습니다.
"가장 기본적인 방식부터 알아볼까요? 이걸 Stop-and-Wait라고 해요."
Stop-and-Wait ARQ는 가장 단순한 오류 제어 방식입니다. 한 번에 하나의 패킷만 보내고, ACK를 받을 때까지 기다린 후 다음 패킷을 보냅니다.
마치 전화 통화에서 한 문장을 말하고 상대방이 "응"이라고 대답할 때까지 기다리는 것과 같습니다.
다음 코드를 살펴봅시다.
class StopAndWaitARQ:
def __init__(self):
self.current_seq = 0 # 0과 1을 번갈아 사용
def send_packet(self, data):
while True:
# 현재 시퀀스 번호로 패킷 전송
packet = {"seq": self.current_seq, "data": data}
self._transmit(packet)
print(f"패킷 전송: seq={self.current_seq}")
# ACK 대기 (타임아웃 포함)
ack = self._wait_for_ack(timeout=2.0)
if ack and ack["ack_num"] == self.current_seq:
# 올바른 ACK 수신, 시퀀스 번호 토글
self.current_seq = 1 - self.current_seq
print("ACK 수신 완료, 다음 패킷으로")
return True
print("타임아웃 또는 잘못된 ACK, 재전송")
박시니어 씨가 화이트보드에 두 개의 컴퓨터를 그렸습니다. 송신자와 수신자입니다.
그리고 그 사이에 화살표를 하나 그렸습니다. "Stop-and-Wait는 말 그대로예요.
멈추고 기다리는 거죠." 송신자가 패킷 하나를 보내면, 수신자가 ACK를 보낼 때까지 아무것도 하지 않고 기다립니다. 김개발 씨는 바로 질문을 던졌습니다.
"그러면 엄청 비효율적이지 않나요? 기다리는 동안 네트워크가 놀잖아요." 박시니어 씨가 미소 지었습니다.
"바로 그게 Stop-and-Wait의 단점이에요." 이 방식을 이해하기 위해 편의점 택배 접수를 떠올려봅시다. 손님이 택배를 맡기면 직원이 접수 완료 확인서를 줍니다.
손님은 확인서를 받기 전까지 다음 택배를 맡길 수 없습니다. 한 번에 하나씩만 처리되는 것입니다.
Stop-and-Wait에서 시퀀스 번호는 0과 1만 사용합니다. 왜 그럴까요?
한 번에 하나의 패킷만 전송 중이기 때문입니다. 현재 패킷이 0번이면, 다음 패킷은 1번, 그 다음은 다시 0번입니다.
이 간단한 번호 체계로 중요한 문제를 해결합니다. 바로 중복 패킷 문제입니다.
만약 ACK가 손실되어 송신자가 같은 패킷을 재전송하면 어떻게 될까요? 수신자는 시퀀스 번호를 보고 "아, 이건 이미 받은 패킷이네"라고 판단할 수 있습니다.
코드를 살펴보면, current_seq 변수가 0과 1 사이를 토글합니다. 1 - self.current_seq라는 간단한 연산으로 번호를 전환합니다.
ACK를 받으면 시퀀스를 바꾸고, 받지 못하면 같은 시퀀스로 재전송합니다. 그렇다면 Stop-and-Wait의 효율성은 어느 정도일까요?
네트워크 이용률을 계산해보면 충격적입니다. 만약 서울에서 부산까지 데이터를 보내는 데 왕복 20ms가 걸린다면, 그 시간 동안 송신자는 아무것도 못 합니다.
특히 위성 통신처럼 지연이 큰 환경에서는 더 심각합니다. 왕복 시간이 500ms라면, 1초에 겨우 2개의 패킷만 보낼 수 있습니다.
아무리 대역폭이 넓어도 속도는 처참해집니다. "그래서 실제로는 이 방식을 잘 안 쓰나요?" 김개발 씨가 물었습니다.
"네, 하지만 개념을 이해하는 출발점으로는 완벽해요. 다음에 배울 방식들이 이걸 어떻게 개선했는지 보면 감탄하게 될 거예요."
실전 팁
💡 - Stop-and-Wait는 구현이 단순해서 학습용이나 저속 통신에 적합합니다
- 네트워크 지연이 큰 환경에서는 반드시 다른 ARQ 방식을 사용하세요
3. Go-Back-N ARQ
김개발 씨는 Stop-and-Wait의 비효율성에 답답함을 느꼈습니다. "분명 더 좋은 방법이 있을 것 같은데요." 박시니어 씨가 고개를 끄덕이며 새로운 그림을 그리기 시작했습니다.
"맞아요, Go-Back-N이라는 방식이 있어요. 이건 여러 패킷을 한꺼번에 보낼 수 있죠."
Go-Back-N ARQ는 ACK를 기다리지 않고 여러 개의 패킷을 연속으로 보내는 방식입니다. 마치 편지를 여러 통 한꺼번에 보내고, 문제가 생긴 편지부터 다시 보내는 것과 같습니다.
윈도우라는 개념을 사용해 동시에 전송 중인 패킷 수를 관리합니다.
다음 코드를 살펴봅시다.
class GoBackN:
def __init__(self, window_size=4):
self.window_size = window_size # 윈도우 크기
self.base = 0 # 윈도우 시작점
self.next_seq = 0 # 다음 전송할 시퀀스
self.buffer = [] # 전송 버퍼
def send(self, packets):
self.buffer = packets
while self.base < len(packets):
# 윈도우 내에서 패킷 전송
while self.next_seq < self.base + self.window_size \
and self.next_seq < len(packets):
self._transmit(packets[self.next_seq])
self.next_seq += 1
ack = self._wait_for_ack()
if ack:
self.base = ack + 1 # 윈도우 슬라이드
else:
# 타임아웃: base부터 재전송
self.next_seq = self.base
print(f"Go-Back-N: {self.base}번부터 재전송")
박시니어 씨가 화이트보드에 새로운 그림을 그렸습니다. 이번에는 화살표가 여러 개입니다.
패킷 1, 2, 3, 4가 연속으로 날아가고 있습니다. "Go-Back-N의 핵심은 파이프라이닝이에요.
한 번에 여러 패킷을 네트워크에 밀어 넣는 거죠." 김개발 씨의 눈이 반짝였습니다. 이게 바로 그가 찾던 해결책 같았습니다.
이 방식을 이해하려면 공장의 컨베이어 벨트를 떠올려보세요. 제품이 하나씩 벨트 위를 지나가며 검수를 받습니다.
문제가 있는 제품이 발견되면, 그 제품부터 뒤에 있는 모든 제품을 다시 검사합니다. 윈도우 크기가 4라면, 동시에 4개의 패킷이 "전송 중" 상태가 될 수 있습니다.
첫 번째 패킷의 ACK를 받기 전에 2, 3, 4번 패킷을 미리 보내는 것입니다. ACK가 도착하면 윈도우가 앞으로 슬라이드합니다.
그런데 만약 2번 패킷이 손실되면 어떻게 될까요? 수신자는 3번, 4번 패킷을 받아도 처리하지 않습니다.
순서대로 받아야 하기 때문입니다. 송신자는 타임아웃 후 2번부터 다시 전송합니다.
이것이 Go-Back-N이라는 이름의 유래입니다. 문제가 생긴 지점으로 되돌아가서 다시 시작하는 것입니다.
2번이 손실되면 2, 3, 4번을 모두 재전송합니다. 3, 4번은 이미 보냈지만 다시 보내야 합니다.
"잠깐요, 그러면 3번, 4번은 낭비 아닌가요? 이미 잘 도착했는데?" 김개발 씨가 날카로운 질문을 던졌습니다.
박시니어 씨가 웃었습니다. "맞아요, 그게 Go-Back-N의 단점이에요." 코드에서 base 변수는 윈도우의 시작점을 나타냅니다.
ACK를 받으면 base가 증가하고 윈도우가 앞으로 이동합니다. 타임아웃이 발생하면 next_seq를 base로 되돌려서 재전송을 시작합니다.
수신자 입장에서 Go-Back-N은 비교적 단순합니다. 순서대로 패킷을 받고, 다음에 기대하는 시퀀스 번호만 관리하면 됩니다.
순서가 맞지 않는 패킷은 그냥 버립니다. 실무에서 Go-Back-N은 오류율이 낮은 환경에서 효과적입니다.
패킷 손실이 거의 없다면, 재전송으로 인한 낭비도 거의 없기 때문입니다. 하지만 네트워크가 불안정하다면 다음에 배울 방식이 더 적합합니다.
실전 팁
💡 - 윈도우 크기는 네트워크 대역폭과 지연을 고려하여 설정하세요
- 수신자는 누적 ACK(cumulative ACK)를 사용해 구현을 단순화할 수 있습니다
4. Selective Repeat ARQ
김개발 씨는 Go-Back-N의 재전송 낭비가 마음에 걸렸습니다. "3번, 4번 패킷이 잘 도착했는데 왜 다시 보내야 하죠?" 박시니어 씨가 고개를 끄덕였습니다.
"좋은 지적이에요. 그래서 나온 게 Selective Repeat입니다.
선택적으로 재전송하는 방식이죠."
Selective Repeat ARQ는 손실된 패킷만 선택적으로 재전송하는 방식입니다. 수신자가 순서에 상관없이 패킷을 받아 버퍼에 저장하고, 나중에 순서대로 재조립합니다.
Go-Back-N보다 효율적이지만 구현이 복잡합니다.
다음 코드를 살펴봅시다.
class SelectiveRepeat:
def __init__(self, window_size=4):
self.window_size = window_size
self.send_base = 0
self.recv_base = 0
self.recv_buffer = {} # 순서 무관하게 버퍼링
self.acked = set() # ACK 받은 패킷 추적
def receiver_handle(self, packet):
seq = packet["seq"]
# 윈도우 범위 내의 패킷만 수락
if self.recv_base <= seq < self.recv_base + self.window_size:
self.recv_buffer[seq] = packet["data"]
self._send_ack(seq) # 개별 ACK 전송
# 순서대로 전달 가능한 패킷 처리
while self.recv_base in self.recv_buffer:
self._deliver(self.recv_buffer.pop(self.recv_base))
self.recv_base += 1
print(f"패킷 {self.recv_base-1} 전달 완료")
박시니어 씨가 새로운 시나리오를 설명하기 시작했습니다. "패킷 1, 2, 3, 4를 보냈는데 2번만 손실됐다고 가정해볼게요." Go-Back-N에서는 2, 3, 4번을 모두 재전송해야 했습니다.
하지만 Selective Repeat에서는 2번만 재전송합니다. 3번과 4번은 이미 수신자의 버퍼에 저장되어 있기 때문입니다.
이것을 택배 시스템으로 비유해봅시다. 여러 상자를 보냈는데 2번 상자만 분실되었습니다.
Go-Back-N 방식이라면 2, 3, 4번 상자를 모두 다시 보내야 합니다. 하지만 Selective Repeat 방식은 2번 상자만 다시 보냅니다.
수신자는 어떻게 이것이 가능할까요? 비결은 버퍼입니다.
3번, 4번 패킷이 도착하면 일단 버퍼에 저장해둡니다. 2번이 도착하면 그제야 2, 3, 4번을 순서대로 상위 계층에 전달합니다.
코드에서 recv_buffer는 딕셔너리로 구현되어 있습니다. 패킷이 도착하면 시퀀스 번호를 키로 저장합니다.
그리고 recv_base부터 연속된 패킷이 있는지 확인하여 순서대로 전달합니다. 송신자 입장에서도 변화가 있습니다.
Go-Back-N에서는 누적 ACK만 받았지만, Selective Repeat에서는 개별 ACK를 받습니다. 3번 패킷의 ACK는 "3번 잘 받았어요"만 의미하지, "1, 2, 3번 다 받았어요"를 의미하지 않습니다.
"그러면 Selective Repeat가 무조건 좋은 거네요?" 김개발 씨가 물었습니다. 박시니어 씨가 고개를 저었습니다.
"장단점이 있어요." Selective Repeat의 단점은 복잡성입니다. 송신자와 수신자 모두 더 많은 상태를 관리해야 합니다.
수신자는 버퍼 공간이 필요하고, 송신자는 각 패킷별로 타이머를 관리해야 합니다. 또 다른 고려사항은 시퀀스 번호 공간입니다.
Selective Repeat에서는 시퀀스 번호가 윈도우 크기의 최소 2배 이상이어야 합니다. 그렇지 않으면 새 패킷과 재전송된 패킷을 구분할 수 없는 상황이 발생합니다.
실무에서 TCP는 Selective Repeat의 변형을 사용합니다. SACK(Selective Acknowledgment) 옵션을 통해 수신자가 어떤 패킷을 받았는지 상세히 알려줄 수 있습니다.
김개발 씨는 각 방식의 트레이드오프를 이해하기 시작했습니다. "상황에 따라 적절한 방식을 선택해야 하는 거군요." 박시니어 씨가 만족스럽게 고개를 끄덕였습니다.
실전 팁
💡 - 오류율이 높은 네트워크에서는 Selective Repeat가 Go-Back-N보다 효율적입니다
- 시퀀스 번호 공간은 윈도우 크기의 2배 이상으로 설정하세요
5. 흐름 제어: 슬라이딩 윈도우
오류 제어를 마스터한 김개발 씨에게 새로운 문제가 찾아왔습니다. 테스트 중 수신 측 서버가 갑자기 멈추는 현상이 발생한 것입니다.
로그를 확인해보니 메모리가 가득 찼습니다. "아무리 빨리 보내도, 받는 쪽이 처리를 못 하면 소용없죠." 박시니어 씨의 조언이 시작됩니다.
흐름 제어는 송신자가 수신자의 처리 능력에 맞춰 전송 속도를 조절하는 메커니즘입니다. 슬라이딩 윈도우는 이를 구현하는 핵심 기법으로, 수신자가 자신의 버퍼 여유 공간을 송신자에게 알려줍니다.
마치 물탱크에 물을 채울 때 수위를 확인하며 조절하는 것과 같습니다.
다음 코드를 살펴봅시다.
class SlidingWindowFlowControl:
def __init__(self):
self.recv_buffer_size = 65535 # 수신 버퍼 크기
self.recv_buffer_used = 0 # 사용 중인 버퍼
def calculate_window(self):
# 광고 윈도우 계산
available = self.recv_buffer_size - self.recv_buffer_used
return available
def receive_data(self, data):
data_size = len(data)
if data_size <= self.calculate_window():
self.recv_buffer_used += data_size
# ACK와 함께 현재 윈도우 크기 전송
window = self.calculate_window()
return {"ack": True, "window": window}
return {"ack": False, "window": 0} # 버퍼 부족
def process_data(self, size):
# 애플리케이션이 데이터 처리 후 버퍼 해제
self.recv_buffer_used -= size
print(f"버퍼 해제, 현재 윈도우: {self.calculate_window()}")
김개발 씨의 파일 전송 서비스가 드디어 잘 동작하기 시작했습니다. 오류 제어 덕분에 데이터 손실 문제는 해결됐습니다.
그런데 새로운 문제가 발생했습니다. 테스트 중 수신 서버의 메모리 사용량이 급증하더니 결국 서버가 다운됐습니다.
송신 측은 초당 1GB를 보낼 수 있었지만, 수신 측은 초당 100MB밖에 처리하지 못했던 것입니다. 박시니어 씨가 설명을 시작했습니다.
"이건 흐름 제어 문제예요. 송신자가 수신자의 능력을 무시하고 마구 데이터를 보내면 이런 일이 생기죠." 흐름 제어를 이해하려면 수도꼭지와 싱크대를 떠올려보세요.
수도꼭지에서 물이 나오는 속도가 싱크대 배수구로 빠지는 속도보다 빠르면, 싱크대가 넘칩니다. 흐름 제어는 싱크대 수위를 보고 수도꼭지를 조절하는 것과 같습니다.
TCP에서 이를 구현하는 방법이 슬라이딩 윈도우입니다. 수신자는 ACK를 보낼 때 수신 윈도우(rwnd) 값을 함께 알려줍니다.
이 값은 "나 지금 이만큼 더 받을 수 있어요"라는 의미입니다. 송신자는 이 값을 보고 전송량을 조절합니다.
수신 윈도우가 1000바이트라면, ACK를 받기 전까지 최대 1000바이트만 보낼 수 있습니다. 윈도우가 0이 되면 송신을 멈춥니다.
코드에서 calculate_window 함수는 현재 사용 가능한 버퍼 크기를 계산합니다. 전체 버퍼에서 사용 중인 공간을 빼면 됩니다.
이 값이 ACK와 함께 송신자에게 전달됩니다. 흥미로운 상황이 있습니다.
윈도우가 0이 되면 어떻게 될까요? 송신자는 전송을 멈추고 기다립니다.
그런데 수신자가 버퍼를 비웠다는 것을 어떻게 알 수 있을까요? 이를 위해 TCP는 윈도우 프로브를 사용합니다.
송신자가 주기적으로 작은 패킷을 보내 수신자의 윈도우 상태를 확인합니다. 수신자가 응답하면서 새로운 윈도우 크기를 알려줍니다.
실무에서 흔히 발생하는 문제 중 하나가 Silly Window Syndrome입니다. 수신자가 아주 작은 공간이 생길 때마다 윈도우를 업데이트하면, 비효율적인 작은 패킷들이 오가게 됩니다.
이를 해결하기 위해 수신자는 일정 크기 이상의 공간이 생길 때만 윈도우를 업데이트합니다. 송신자도 Nagle 알고리즘을 사용해 작은 데이터를 모아서 보냅니다.
김개발 씨는 이제 왜 서버가 다운됐는지 이해했습니다. "흐름 제어 없이 무작정 보내면 안 되는 거군요!"
실전 팁
💡 - 수신 윈도우 크기는 소켓 버퍼 설정에서 조절할 수 있습니다
- 윈도우 스케일링 옵션을 사용하면 64KB 이상의 윈도우도 지원됩니다
6. 혼잡 제어 알고리즘
김개발 씨는 흐름 제어까지 적용했지만, 또 다른 문제가 발생했습니다. 특정 시간대에 전송 속도가 급격히 느려지는 것입니다.
수신자의 버퍼는 충분한데 말이죠. "이건 수신자 문제가 아니에요.
네트워크 자체가 막히는 거예요." 박시니어 씨가 새로운 주제를 꺼냈습니다.
혼잡 제어는 네트워크 전체의 과부하를 방지하는 메커니즘입니다. 흐름 제어가 수신자를 보호한다면, 혼잡 제어는 네트워크를 보호합니다.
TCP는 슬로우 스타트, 혼잡 회피, 빠른 복구 등의 알고리즘을 조합하여 네트워크 상태에 적응합니다.
다음 코드를 살펴봅시다.
class TCPCongestionControl:
def __init__(self):
self.cwnd = 1 # 혼잡 윈도우 (MSS 단위)
self.ssthresh = 64 # 슬로우 스타트 임계값
self.state = "slow_start"
def on_ack_received(self):
if self.state == "slow_start":
self.cwnd *= 2 # 지수적 증가
if self.cwnd >= self.ssthresh:
self.state = "congestion_avoidance"
print("혼잡 회피 모드 진입")
else:
# 혼잡 회피: 선형적 증가
self.cwnd += 1 / self.cwnd
def on_timeout(self):
# 타임아웃: 심각한 혼잡 신호
self.ssthresh = self.cwnd // 2
self.cwnd = 1
self.state = "slow_start"
print(f"타임아웃! cwnd=1, ssthresh={self.ssthresh}")
def on_triple_dup_ack(self):
# 3중 중복 ACK: 빠른 복구
self.ssthresh = self.cwnd // 2
self.cwnd = self.ssthresh + 3
print(f"빠른 복구, cwnd={self.cwnd}")
박시니어 씨가 새로운 비유를 들었습니다. "고속도로를 생각해보세요.
모든 차가 최고 속도로 달리면 어떻게 될까요?" 김개발 씨가 대답했습니다. "교통 체증이요." 정확합니다.
네트워크도 마찬가지입니다. 모든 컴퓨터가 최대 속도로 데이터를 보내면 라우터의 큐가 가득 차고, 패킷이 버려지며, 결국 모두가 느려집니다.
이것이 네트워크 혼잡입니다. 혼잡 제어의 핵심 아이디어는 간단합니다.
네트워크가 막히기 전에 속도를 줄이자는 것입니다. 문제는 어떻게 네트워크 상태를 알 수 있느냐는 것입니다.
TCP는 간접적인 신호를 사용합니다. 패킷 손실이 발생하면 네트워크가 혼잡하다고 판단합니다.
손실은 타임아웃이나 중복 ACK로 감지합니다. 첫 번째 알고리즘은 슬로우 스타트입니다.
이름과 달리 실제로는 빠릅니다. 혼잡 윈도우(cwnd)를 1에서 시작해 ACK를 받을 때마다 2배씩 늘립니다.
1, 2, 4, 8, 16... 지수적 증가입니다.
왜 이렇게 할까요? 처음에는 네트워크 용량을 모르기 때문입니다.
조심스럽게 시작하되, 문제없으면 빠르게 속도를 올립니다. 하지만 무한히 증가할 수는 없습니다.
**ssthresh(슬로우 스타트 임계값)**에 도달하면 혼잡 회피 모드로 전환됩니다. 이제부터는 선형적으로 증가합니다.
RTT당 1MSS씩만 늘어납니다. 더 조심스럽게 네트워크 용량을 탐색하는 것입니다.
그러다 혼잡이 감지되면 어떻게 될까요? 두 가지 경우가 있습니다.
타임아웃은 심각한 신호입니다. 패킷이 완전히 사라졌다는 의미이기 때문입니다.
이 경우 cwnd를 1로 리셋하고, ssthresh는 현재 cwnd의 절반으로 설정합니다. 처음부터 다시 시작합니다.
3중 중복 ACK는 덜 심각한 신호입니다. 패킷이 손실됐지만 이후 패킷들은 도착했다는 의미입니다.
이 경우 빠른 복구 알고리즘을 사용합니다. cwnd를 절반으로 줄이되, 1로 리셋하지는 않습니다.
코드를 보면 이 세 가지 상황이 각각 다른 메서드로 처리됩니다. on_ack_received는 정상 상황, on_timeout은 타임아웃, on_triple_dup_ack는 빠른 복구를 담당합니다.
"그러면 결국 속도가 들쭉날쭉하겠네요?" 김개발 씨가 물었습니다. "맞아요, TCP의 전송 속도 그래프를 보면 톱니 모양이에요.
올라갔다가 혼잡으로 떨어지고, 다시 올라가고..."
실전 팁
💡 - 혼잡 윈도우와 수신 윈도우 중 작은 값이 실제 전송 윈도우가 됩니다
- 현대 TCP는 CUBIC, BBR 등 더 발전된 알고리즘을 사용합니다
7. ECN(명시적 혼잡 알림)
김개발 씨는 혼잡 제어를 공부하면서 한 가지 의문이 들었습니다. "패킷이 손실되어야만 혼잡을 아는 건 너무 늦은 거 아닌가요?
손실 전에 미리 알 수 있으면 좋을 텐데요." 박시니어 씨가 미소 지었습니다. "바로 그 생각으로 만들어진 게 ECN이에요."
**ECN(Explicit Congestion Notification)**은 라우터가 패킷을 버리기 전에 혼잡 상태를 송신자에게 명시적으로 알려주는 메커니즘입니다. 마치 고속도로의 전광판이 "정체 구간 앞에 있음"을 미리 알려주는 것과 같습니다.
패킷 손실 없이도 혼잡에 대응할 수 있어 효율적입니다.
다음 코드를 살펴봅시다.
class ECNHandler:
# IP 헤더의 ECN 필드 값
NOT_ECT = 0b00 # ECN 미지원
ECT_0 = 0b10 # ECN 지원
ECT_1 = 0b01 # ECN 지원 (대체)
CE = 0b11 # 혼잡 경험
def router_handle(self, packet, queue_length, threshold):
# 라우터에서의 ECN 처리
if queue_length > threshold:
if packet["ecn"] in [self.ECT_0, self.ECT_1]:
packet["ecn"] = self.CE # 혼잡 표시
print("ECN: 혼잡 표시 설정")
return packet # 패킷 유지
else:
return None # ECN 미지원시 드롭
return packet
def receiver_handle(self, packet, tcp_header):
# 수신자: CE 플래그 확인 후 ACK에 ECE 설정
if packet["ecn"] == self.CE:
tcp_header["ECE"] = True
return tcp_header
def sender_handle(self, ack):
# 송신자: ECE 확인 후 혼잡 대응
if ack.get("ECE"):
print("ECN 혼잡 알림 수신, 윈도우 감소")
return True # 혼잡 대응 필요
return False
기존 혼잡 제어의 문제점을 다시 생각해봅시다. 패킷 손실이 발생해야만 혼잡을 인지합니다.
이미 늦은 것입니다. 손실된 패킷은 재전송해야 하고, 지연이 발생합니다.
박시니어 씨가 비유를 들었습니다. "비가 와서 길이 미끄러워진 후에야 속도를 줄이는 것과, 날씨 예보를 보고 미리 속도를 줄이는 것, 어느 쪽이 안전할까요?" ECN은 바로 그 날씨 예보 역할을 합니다.
라우터가 "지금 내 큐가 많이 찼어요, 조금 천천히 보내주세요"라고 미리 알려주는 것입니다. 어떻게 작동하는지 단계별로 살펴봅시다.
먼저 송신자와 수신자가 TCP 연결을 맺을 때 ECN 지원 여부를 협상합니다. 양쪽 모두 지원하면 ECN을 사용합니다.
송신자는 IP 헤더의 ECN 필드를 ECT(ECN-Capable Transport)로 설정하여 패킷을 보냅니다. 이것은 "이 패킷은 ECN을 지원해요"라는 표시입니다.
라우터는 자신의 큐가 임계값을 넘으면 결정을 내려야 합니다. 기존에는 패킷을 버렸습니다.
하지만 ECN을 지원하는 패킷이라면 다르게 처리합니다. 라우터는 패킷의 ECN 필드를 **CE(Congestion Experienced)**로 바꿉니다.
패킷은 버리지 않고 그대로 전달합니다. "혼잡을 경험했어요"라는 도장을 찍어서 보내는 것입니다.
수신자가 이 패킷을 받으면 ECN 필드를 확인합니다. CE가 설정되어 있으면, ACK를 보낼 때 TCP 헤더의 ECE(ECN-Echo) 플래그를 설정합니다.
"방금 받은 패킷이 혼잡을 경험했대요"라고 송신자에게 전달하는 것입니다. 송신자는 ECE 플래그가 설정된 ACK를 받으면 혼잡 대응을 시작합니다.
윈도우를 줄이고, CWR(Congestion Window Reduced) 플래그를 설정하여 "알겠어요, 줄였어요"라고 응답합니다. 코드에서 router_handle 함수를 보면, 큐가 임계값을 넘었을 때 ECT 패킷은 CE로 표시하고, 그렇지 않은 패킷은 드롭합니다.
ECN 미지원 패킷에 대해서는 기존 방식대로 처리합니다. ECN의 장점은 명확합니다.
패킷 손실이 없으므로 재전송도 없습니다. 혼잡을 더 빨리 감지하므로 반응도 빠릅니다.
특히 데이터센터처럼 지연이 중요한 환경에서 큰 효과를 발휘합니다. 하지만 ECN도 한계가 있습니다.
경로상의 모든 장비가 ECN을 지원해야 합니다. 일부 방화벽이나 오래된 라우터는 ECN 비트가 설정된 패킷을 잘못 처리할 수 있습니다.
김개발 씨가 감탄했습니다. "결국 서로 협력해서 네트워크를 건강하게 유지하는 거군요!" 박시니어 씨가 고개를 끄덕였습니다.
"맞아요, 그게 바로 TCP 설계의 아름다운 점이에요."
실전 팁
💡 - ECN은 리눅스에서 sysctl로 활성화할 수 있습니다 (net.ipv4.tcp_ecn)
- DCTCP는 데이터센터를 위해 ECN을 적극 활용하는 알고리즘입니다
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
Helm 마이크로서비스 패키징 완벽 가이드
Kubernetes 환경에서 마이크로서비스를 효율적으로 패키징하고 배포하는 Helm의 핵심 기능을 실무 중심으로 학습합니다. Chart 생성부터 릴리스 관리까지 체계적으로 다룹니다.
보안 아키텍처 구성 완벽 가이드
프로젝트의 보안을 처음부터 설계하는 방법을 배웁니다. AWS 환경에서 VPC부터 WAF, 암호화, 접근 제어까지 실무에서 바로 적용할 수 있는 보안 아키텍처를 단계별로 구성해봅니다.
AWS Organizations 완벽 가이드
여러 AWS 계정을 체계적으로 관리하고 통합 결제와 보안 정책을 적용하는 방법을 실무 스토리로 쉽게 배워봅니다. 초보 개발자도 바로 이해할 수 있는 친절한 설명과 실전 예제를 제공합니다.
AWS KMS 암호화 완벽 가이드
AWS KMS(Key Management Service)를 활용한 클라우드 데이터 암호화 방법을 초급 개발자를 위해 쉽게 설명합니다. CMK 생성부터 S3, EBS 암호화, 봉투 암호화까지 실무에 필요한 모든 내용을 담았습니다.
AWS Secrets Manager 완벽 가이드
AWS에서 데이터베이스 비밀번호, API 키 등 민감한 정보를 안전하게 관리하는 Secrets Manager의 핵심 개념과 실무 활용법을 배워봅니다. 초급 개발자도 쉽게 따라할 수 있도록 실전 예제와 함께 설명합니다.