Huny's Dev Blog
huny.dev

TCP_NODELAY는 항상 켜는 게 좋을까?

Nagle과 delayed ACK의 차이, Go의 기본값, 애플리케이션 버퍼를 구분하고 Linux 소켓 옵션을 직접 확인한다.

Hun Jang
Hun Jang Oct 27, 2026

데이터를 모아 보내는 방식과 지연을 표현한 AI 생성 개념 이미지. 실제 패킷을 측정한 화면은 아니다.

TCP 소켓 예제를 보다 보면 TCP_NODELAY를 켜는 코드가 자주 등장한다. 이름만 보면 켜 두기만 해도 네트워크가 빨라질 것 같다. 그렇다면 패킷을 모아 보내는 Nagle 알고리즘은 이제 쓸모가 없어진 것일까?

결론부터 말하면 작은 메시지의 응답 시간이 중요한 연결에서는 NODELAY가 합리적인 출발점이지만, 모든 프로그램의 정답은 아니다. 이 옵션이 없애는 것은 Nagle 알고리즘에 의한 대기다. 애플리케이션 안의 버퍼나 네트워크의 모든 지연을 없애 주지는 않는다.

Nagle은 무엇을 기다리는가

아주 작은 데이터를 매번 독립적으로 보내면 데이터에 비해 헤더와 패킷 처리 비용이 커진다. Nagle 알고리즘은 아직 확인 응답을 받지 못한 데이터가 있을 때 작은 후속 데이터를 모았다가, ACK가 오거나 충분한 크기가 되면 전송하는 방식이다.

여기서 수신 측의 delayed ACK와 구분해야 한다. Nagle은 송신 데이터를 모으는 동작이고, delayed ACK는 수신 측에서 ACK를 잠시 늦추는 동작이다. 두 동작이 특정 요청·응답 패턴에서 맞물리면 기대하지 않은 대기가 생길 수 있다.

그렇다고 “Nagle은 무조건 200ms 기다리는 타이머”라고 외우면 곤란하다. 고정된 지연 시간으로 정의된 기능이 아니며, 실제 결과는 구현과 통신 패턴에 따라 달라진다. 동작의 기준은 RFC 9293의 작은 세그먼트 처리와 ACK 생성 규칙에서 확인할 수 있다.

NODELAY를 켜도 모든 버퍼가 사라지지는 않는다

TCP_NODELAY=1은 Nagle 알고리즘을 비활성화한다. 작은 데이터를 Nagle 때문에 더 모으지 않고 가능한 한 빨리 보내도록 하는 설정이다. 다만 혼잡 제어와 수신 윈도 같은 TCP의 다른 제약까지 건너뛰지는 않는다. Linux에서는 TCP_CORK와 함께 사용할 때의 우선순위도 고려해야 한다. Linux tcp(7)

애플리케이션 버퍼는 한 단계 더 앞에 있다. 예를 들어 Go에서 bufio.Writer에 쓴 데이터가 아직 애플리케이션 버퍼에 남아 있다면, NODELAY를 켜도 그 데이터를 대신 꺼내 보내지 않는다. Flush()는 버퍼에 남은 데이터를 하위 Writer로 전달하는 함수다. 상대방이 이미 받았다는 뜻은 아니다. Go bufio.Writer.Flush

응답이 늦을 때는 소켓 옵션부터 바꾸기보다, 먼저 데이터가 어느 계층에 머물러 있는지 확인하는 편이 낫다.

Go에서는 이미 기본값이 true다

Go 공식 net 패키지의 SetNoDelay 설명을 직접 캡처했다. 기본값이 true임을 명시하고 있다.

Go의 일반적인 net.TCPConn은 SetNoDelay의 기본값이 true다. 따라서 “TCP는 기본적으로 Nagle을 쓰니 Go에서도 반드시 켜야 한다”는 설명은 정확하지 않다. 운영체제에서 직접 만든 소켓의 기본값과 언어 런타임이 연결을 설정하는 방식은 별개다. Go TCPConn.SetNoDelay

반대로 라이브러리나 기존 코드가 값을 바꿨을 가능성도 있다. 단순히 설정 코드를 추가하기보다 연결을 만드는 경로와 이미 적용된 설정을 먼저 보는 것이 좋다.

소켓 옵션만 작게 확인해 보기

다음 코드는 외부 서버에 접속하지 않는다. Python으로 TCP 소켓을 하나 만들고 옵션 값을 읽은 뒤, 켜고 끄면서 다시 확인한다.

이 글을 준비하며 Linux 환경에서 직접 실행한 결과는 다음과 같다.

이 결과로 확인한 것은 해당 환경에서 생성한 소켓의 옵션 값과 변경 여부뿐이다. 실제 데이터를 보내지 않았으므로 응답 시간이 줄었다거나 패킷 수가 늘었다는 증거는 아니다. Go의 기본값 역시 이 Python 실험으로 검증한 것이 아니라 앞의 공식 문서로 확인한 내용이다.

write 한 번이 패킷 하나는 아니다

NODELAY를 켜면 write()할 때마다 상대방의 read()가 같은 크기로 돌아올 것이라고 생각하기 쉽다. 하지만 TCP는 바이트 스트림이다. 쓰기 경계와 세그먼트 경계, 읽기 경계가 일치한다고 보장하지 않는다. 메시지 구분은 길이 접두어나 구분자 같은 애플리케이션 프로토콜이 담당해야 한다. RFC 9293 §3.7

지연을 줄이겠다고 헤더와 본문을 불필요하게 잘게 쪼개 쓰는 것도 좋은 전략은 아니다. 작은 쓰기가 작은 패킷을 늘리는 조건에서는 패킷 처리 비용이 커질 수 있다. 논리적으로 한 메시지인 데이터는 가능한 범위에서 모으되, 다음 메시지가 올 때까지 무작정 기다리지는 않는 방식을 먼저 고려할 만하다. Red Hat의 TCP_NODELAY와 작은 쓰기 안내

무엇을 기준으로 결정할까

요청 하나를 보내고 짧은 응답을 기다리는 서비스라면 NODELAY를 켠 상태를 출발점으로 삼을 수 있다. 반면 대량 데이터를 전송하는 경로에서는 이 옵션 하나보다 쓰기 크기와 버퍼링 전략이 더 중요한 문제가 될 수 있다. 이는 앞의 동작 원리를 바탕으로 한 선택 기준이지, 모든 서비스에 그대로 적용할 성능 보장은 아니다.

비교할 때는 실제 메시지 크기, 쓰기 빈도, RTT, 동시 접속 수를 맞춘 뒤 다음을 함께 본다.

  • 평균뿐 아니라 p95·p99 응답 시간
  • 초당 처리량과 CPU 사용량
  • 패킷 수와 애플리케이션의 쓰기 패턴
  • 버퍼를 비우는 시점과 요청 경계

결국 질문은 “NODELAY가 더 빠른가?”보다 “지금 이 연결에서 무엇을 기다리고 있는가?”에 가깝다. 이미 켜져 있는 옵션을 다시 켜는 것보다, 버퍼와 실제 쓰기 패턴을 확인하는 일이 먼저다.

추천 글