유성

네트워크/IO 퍼포먼스 (Zero-Copy) 본문

Architecture

네트워크/IO 퍼포먼스 (Zero-Copy)

백엔드 유성 2025. 12. 21. 22:47

백엔드 개발을 하다 보면 마주하는 성능 병목의 지점은 대부분 CPU 로직이 아닌 I/O(Input/Output)에서 발생한다.

 

특히 대용량 파일을 전송하거나 고성능 메시징 시스템을 설계할 때, "데이터를 얼마나 빨리 읽느냐" 보다 중요한 것은

"데이터를 얼마나 덜 복사하느냐" 이다.

애플리케이션의 응답 지연(Latency)을 줄이고 처리량(Throughput)을 극대화하기 위한 핵심 기술, Zero-Copy에 대해 다뤄보고자 한다.

 

1. 전통적인 I/O: 데이터는 생각보다 많이 '복사'된다

우리가 흔히 사용하는 read()와 write() 시스템 콜은 겉보기에 단순해 보이지만, 실제 데이터 이동 경로는 매우 복잡하다.

표준 I/O모델에서는 데이터가 하드웨어에서 네트워크로 전달되기까지 총 4번의 복사4번의 컨텍스트 스위칭이 발생한다.

  1. 하드웨어 → 커널 버퍼: OS 커널 영역으로 데이터 로드
  2. 커널 버퍼 → 유저 버퍼: 애플리케이션이 데이터를 쓰기 위해 유저 영역으로 복사
  3. 유저 버퍼 → 소켓 버퍼: 전송을 위해 소켓 버퍼로 다시 복사
  4. 소켓 버퍼 → 프로토콜 엔진: 실제 네트워크 전송

이 과정에서 CPU는 단순한 데이터 복사를 위해 불필요한 사이클을 낭비하고, 유저 영역으로 올라온 데이터는 곧바로 GC의 대상이 되어 Heap 메모리 압박과 OOM 리스크를 유발한다.

 

2. Zero-Copy: 유저 영역을 거치지 않는 방법

Zero-Copy의 핵심은 "데이터가 유저 모드를 거치지 않고 커널 영역 내에서 직접 이동"하게 만드는 것이다.

이를 통해 CPU 복사 횟수를 0으로 줄이고, 컨텍스트 스위칭 횟수를 획기적으로 낮춘다.

 

Java NIO에서는 주로 두 가지 방식을 통해 이를 구현한다.

  • mmap() (Memory Mapped File): 커널 영역의 버퍼를 유저 영역과 공유하여 '복사' 과정 자체를 제거
  • sendfile() (Java의 FileChannel.transferTo()): 커널 버퍼에서 소켓 버퍼로 데이터를 직접 쏘아 올린다. 데이터가 유저 영역을 전혀 거치지 않으므로 CPU는 전송 명령만 내릴 뿐 실제 복사 작업에는 관여하지 않는다.

아래 코드는 transferTo를 사용하여 file 데이터를 (복사 없이) server로 전송하는 예시이다.

(Socket 또는 ServerSocket에서 사용하는 stream의 경우 유저 영역의 copy(byte[])를 포함하고 있어, Zero-Copy의 경우 Channel을 사용해야 한다)

fun sendFileUsingZeroCopy(filePath: String, host: String, port: Int) {
    val file = File(filePath)
    val serverAddress = InetSocketAddress(host, port)

    // 1. 서버 소켓 채널 오픈
    val socketChannel = SocketChannel.open()
    socketChannel.connect(serverAddress)

    // 2. 파일 채널 오픈 (읽기 전용)
    val fileChannel = RandomAccessFile(file, "r").channel

    val position = 0L
    val size = fileChannel.size()
    // 3. zero-copy 전송
    val transferredBytes = fileChannel.transferTo(position, size, socketChannel)

    fileChannel.close()
    socketChannel.close()
}

 

 

3. 왜 Zero-Copy를 알아야 하는가?

Zero-Copy는 단순히 이론적인 기술이 아니며, 우리가 사용하는 고성능 오픈소스의 근간이 되기 때문이다.

  • Apache Kafka: 카프카가 초당 수백만 건의 메시지를 처리하는 비결은 메시지 소비(Consume) 시 sendfile() 기반의 Zero-Copy를 활용해 디스크 캐시 데이터를 소켓으로 직접 전달한다.
  • Netty (NIO): Webflux와 같이 사용하는 웹 서버로 채널을 이용함으로써 각 요청이 각 쓰레드에 매핑되어있지 않아도되어 고가용성을 확보한다.

 

4. 실전에서의 트레이드오프: 은탄환은 없다

Zero-Copy가 만능은 아니다. 데이터 내용을 유저 영역으로 가져오지 않기 때문에,

애플리케이션 단에서 보안, 검증, 데이터 암호화, 압축하는 등의 가공이 필요하다면 Zero-Copy의 장점을 이용하기 어렵다.

 

결국 데이터의 내용을 수정하지 않고 그대로 전달하는가? 가 Zero-Copy 도입의 결정적 기준이 된다.

 

예를 들어서 웹서버에 이미지를 전달하고 웹서버는 이미지를 바로 저장하는 환경인 경우 장점을 활용할 수 있다.

애플리케이션(유저 영역)을 거치지 않고, 커널버퍼 → 파일 버퍼로 바로 이동 이 가능하다.