비동기 파일 입출력의 필요성

대용량 파일을 처리하는 작업에서 비동기 프로그래밍이 중요한 이유는 I/O 작업의 병목 현상을 피하고, 시스템 리소스를 더 효율적으로 사용하기 위함이다. 전통적인 동기식 파일 입출력 방식에서는 파일 읽기나 쓰기 작업이 완료될 때까지 프로그램의 다른 작업들이 대기 상태에 놓인다. 이는 특히 파일 크기가 클수록, 혹은 네트워크를 통한 입출력일 경우 더 큰 문제로 작용한다.

비동기 작업을 도입함으로써, 파일 입출력 작업이 진행되는 동안 CPU는 다른 작업을 수행할 수 있다. 이는 멀티스레드 환경에서도 중요하며, Boost.Asio 라이브러리를 사용하여 비동기 입출력을 구현함으로써 파일 처리 성능을 크게 향상시킬 수 있다.

Boost.Asio와 비동기 파일 처리

Boost.Asio는 네트워크 작업뿐만 아니라 파일 입출력에도 사용될 수 있는 강력한 비동기 프로그래밍 라이브러리이다. Boost.Asio에서 제공하는 async_readasync_write 함수는 비동기 방식으로 파일을 읽고 쓸 수 있는 메커니즘을 제공한다. 이를 통해 대용량 파일 처리에서 효율적인 자원 관리가 가능해진다.

비동기 파일 처리를 위해서는 파일 디스크립터를 사용하며, 이를 관리하기 위해 boost::asio::posix::stream_descriptor를 이용할 수 있다. 이 클래스는 파일 스트림을 관리하고, async_read_some, async_write_some 등의 비동기 작업을 수행할 수 있도록 돕는다.

boost::asio::posix::stream_descriptor file_descriptor(io_context, ::open("large_file.txt", O_RDWR));

비동기 파일 입출력과 버퍼 관리

대용량 파일을 비동기적으로 처리할 때, 파일을 한꺼번에 읽거나 쓰는 것은 비효율적일 수 있다. 따라서 버퍼를 사용하여 데이터를 일정 크기씩 처리하는 방식이 일반적이다. Boost.Asio는 이러한 버퍼 관리에 적합한 인터페이스를 제공하며, boost::asio::buffer를 사용하여 데이터를 읽고 쓸 수 있다.

버퍼 크기는 I/O 성능에 영향을 미치는 중요한 요소이다. 너무 작은 버퍼를 사용하면 I/O 작업이 빈번해져 오히려 성능이 저하될 수 있고, 너무 큰 버퍼는 메모리 낭비를 초래할 수 있다. 적절한 버퍼 크기를 선택하는 것이 중요하다.

예를 들어, 4KB 크기의 버퍼를 사용하여 비동기적으로 파일을 읽는 코드는 다음과 같다.

char data[4096];
boost::asio::async_read(file_descriptor, boost::asio::buffer(data), 
    [](boost::system::error_code ec, std::size_t bytes_transferred) {
        if (!ec) {
            // 데이터를 성공적으로 읽었을 때의 처리
        }
    });

비동기 작업의 효율성 평가

비동기 프로그래밍의 효율성은 주로 CPU 사용률, 파일 입출력 대기 시간, 그리고 전체적인 작업 처리 시간으로 평가된다. 비동기 방식은 특히 다중 작업을 동시에 처리할 때 이점이 크며, 이는 수학적으로 표현될 수 있다.

비동기 파일 처리에서의 I/O 대기 시간은 다음과 같이 나타낼 수 있다.

T_{total} = T_{cpu} + T_{io}

여기서:

비동기 처리를 통해 T_{io} 동안 CPU가 유휴 상태에 놓이지 않고 다른 작업을 병행함으로써 전체 처리 시간이 감소한다.

멀티스레드와 비동기 파일 처리의 조합

멀티스레드를 이용하여 대용량 파일을 병렬로 처리하는 방식도 성능을 향상시킬 수 있다. 각 스레드가 비동기적으로 파일의 일부분을 처리하면, 스레드 간의 작업 분할과 비동기 작업이 결합되어 매우 높은 처리량을 얻을 수 있다. 이때 스레드 간의 동기화 문제가 발생할 수 있으므로 이를 적절히 관리하는 것이 중요하다.

비동기 처리의 병렬성 모델

대용량 파일 처리에서 비동기성과 멀티스레딩을 결합할 때, 병렬성 모델을 이해하는 것이 중요하다. 비동기 프로그래밍은 CPU가 한 작업이 완료되기를 기다리지 않고 다른 작업을 수행할 수 있도록 하는데, 이를 수학적으로는 병렬성의 관점에서 설명할 수 있다. 일반적으로 파일 처리에서 비동기 프로그래밍은 비동기 작업 A_i들이 있을 때, 각 작업을 다음과 같이 병렬로 수행할 수 있다.

P(T) = \sum_{i=1}^{n} A_i(T)

여기서:

비동기 작업이 모두 병렬로 처리된다면, 처리량은 이론적으로 선형적으로 증가할 수 있다. 하지만, 실제로는 파일 시스템의 I/O 대역폭, 메모리 한계, CPU 코어 수에 따라 처리량 증가가 제한될 수 있다. 이와 관련된 오버헤드와 자원 경합을 고려해야 한다.

멀티스레드와 비동기 작업을 결합할 경우, 병렬 작업 스케줄러의 역할이 매우 중요해진다. 작업이 스레드 풀에서 관리되기 때문에 각 스레드가 적절한 비동기 작업을 할당받아야 하고, 작업 간의 경합을 최소화하여 성능을 최대화해야 한다.

I/O 작업 스케줄링

비동기 파일 처리에서 효율적인 스케줄링은 필수적이다. I/O 작업 스케줄링은 각 스레드와 비동기 작업이 언제 실행되어야 할지를 결정하는데, 이는 작업 큐와 이벤트 루프를 통해 처리된다. Boost.Asio는 이를 위한 효율적인 이벤트 루프를 제공하여 I/O 작업을 스케줄링하고 관리할 수 있다.

Boost.Asio의 이벤트 루프는 다음과 같은 방식으로 동작한다:

  1. 비동기 작업이 시작되면 해당 작업은 비동기 큐에 등록된다.
  2. I/O 작업이 완료되면 그에 대응하는 콜백 함수가 호출된다.
  3. 이벤트 루프는 남은 작업이 없을 때까지 계속해서 큐를 돌며 작업을 처리한다.

이때 비동기 작업의 특성에 따라 여러 가지 스케줄링 전략이 사용될 수 있다. 예를 들어, 대용량 파일을 처리할 때는 우선순위 스케줄링을 사용하여 중요도가 높은 파일의 작업이 먼저 처리되도록 할 수 있다.

비동기 파일 처리에서의 메모리 관리

대용량 파일을 비동기적으로 처리할 때는 메모리 관리가 중요한 이슈가 된다. 대용량 파일은 많은 메모리를 요구할 수 있으며, 이로 인해 메모리 부족 현상이 발생할 수 있다. 비동기 작업은 메모리 상에서 버퍼를 사용하여 데이터를 읽고 쓴다. 이때 적절한 메모리 할당 및 해제가 이루어져야 한다.

비동기 작업이 완료되면, 사용된 버퍼를 해제하거나 재사용해야 한다. 그렇지 않으면 메모리 누수가 발생할 수 있으며, 이는 시스템의 성능을 저하시킬 수 있다. 따라서, 비동기 작업이 완료될 때마다 메모리를 적절히 관리하는 것이 중요하다. 이를 위해 스마트 포인터를 사용하는 것이 일반적이다. 예를 들어, std::shared_ptr을 사용하여 비동기 작업에서 버퍼를 관리할 수 있다.

Boost.Asio의 성능 최적화

Boost.Asio를 사용하여 대용량 파일을 비동기적으로 처리할 때 성능을 최적화하는 여러 방법이 있다. 그중 가장 중요한 요소는 적절한 I/O 작업의 병렬화와 이벤트 루프의 최적화이다.

  1. 작업 단위 최적화: 비동기 작업을 너무 작게 나누면 오히려 성능이 저하될 수 있다. 각 비동기 작업이 완료된 후 호출되는 콜백 함수는 CPU 작업을 차지하기 때문에, 작업을 적절한 크기로 나누는 것이 중요하다.

  2. 멀티스레드 이벤트 루프: Boost.Asio의 io_context는 기본적으로 단일 스레드에서 동작하지만, 멀티스레드에서 사용되도록 확장할 수 있다. 이 경우, 여러 스레드에서 동시에 I/O 작업을 처리할 수 있어 성능이 크게 향상될 수 있다.

boost::asio::io_context io_context;
std::vector<std::thread> threads;
for (int i = 0; i < num_threads; ++i) {
    threads.emplace_back([&io_context]() { io_context.run(); });
}
  1. I/O 작업의 비동기화: 가능한 한 많은 작업을 비동기화하여 CPU가 유휴 상태에 놓이지 않도록 해야 한다. CPU 바운드 작업과 I/O 바운드 작업을 적절히 분리하여, CPU가 I/O 대기 시간 동안 다른 작업을 수행할 수 있도록 한다.

I/O 경합 문제와 해결책

비동기 파일 처리에서 흔히 발생하는 문제 중 하나는 I/O 경합 문제이다. 여러 스레드가 동시에 동일한 파일에 접근하려고 할 때, 경합이 발생할 수 있다. 이러한 경합은 파일 시스템의 락을 초래하고, 전체 처리 성능을 저하시킨다. 이를 해결하기 위해서는 다음과 같은 전략을 사용할 수 있다.

  1. 파일 분할: 대용량 파일을 처리할 때, 파일을 여러 조각으로 나누어 각 스레드가 서로 다른 부분을 비동기적으로 처리하도록 한다. 이를 통해 경합을 줄일 수 있다.

  2. 락 없는 알고리즘: 가능하다면 락을 사용하지 않는 알고리즘을 채택하여 I/O 경합을 줄인다. 이를 위해 데이터가 동시에 수정되지 않도록 데이터를 읽기 전용으로 처리하거나, 파일에 대한 접근을 비동기적으로 조정하는 방법이 있다.

  3. 락 관리 최적화: 파일 시스템에서 필요한 경우 최소한의 락을 사용하도록 알고리즘을 최적화한다. 예를 들어, 파일 접근이 빈번하지 않은 부분은 락을 사용하고, 빈번한 부분은 락을 피하는 방식이다.

비동기 파일 처리에서 발생할 수 있는 문제점과 해결책

비동기 파일 처리 과정에서는 여러 가지 문제점이 발생할 수 있다. 이 문제들은 대용량 파일을 다룰 때 더욱 두드러지며, 성능뿐만 아니라 안정성에 영향을 미칠 수 있다. 이러한 문제들을 사전에 예측하고, 적절한 해결책을 적용하는 것이 중요하다.

1. 파일 시스템의 한계

파일 시스템에 따라 비동기 작업의 효율성이 달라질 수 있다. 일부 파일 시스템은 동시 파일 접근에 최적화되어 있지 않으며, 비동기 작업이 파일 시스템의 성능 병목을 유발할 수 있다. 이러한 경우, 비동기 작업의 효과가 제한될 수 있으며, 오히려 동기 작업보다 느린 결과를 초래할 수도 있다.

해결책: 파일 시스템의 성능을 최대화할 수 있는 방법을 찾아야 한다. 특정 파일 시스템은 다중 스레드와 비동기 작업에 더 적합하므로, 파일 시스템 선택을 신중히 해야 한다. 또한, 파일 시스템의 최적화 옵션을 적극적으로 활용하여 성능을 향상시킬 수 있다. 예를 들어, ext4 파일 시스템에서 저널링을 비활성화하거나 xfs와 같은 고성능 파일 시스템을 사용할 수 있다.

2. 디스크 I/O 대역폭의 한계

비동기 파일 처리에서 디스크 I/O 대역폭은 결정적인 역할을 한다. 여러 비동기 작업이 동시에 파일을 읽고 쓸 때, 디스크의 I/O 대역폭이 포화 상태에 이를 수 있다. 이 경우, 비동기 작업들이 경쟁하게 되고, 처리 속도가 저하된다.

B_{total} = \min \left( \sum_{i=1}^{n} B_i, B_{disk} \right)

여기서:

이 식에서 알 수 있듯이, 비동기 작업이 증가함에 따라 디스크의 대역폭 한계로 인해 전체 처리량이 제한될 수 있다.

해결책: 이를 해결하기 위해서는 작업의 I/O 대역폭을 적절히 제어해야 한다. Boost.Asio는 비동기 작업의 스로틀링(throttling) 기능을 제공하지 않으므로, 사용자가 직접 대역폭을 제어하는 코드를 작성해야 한다. 예를 들어, 비동기 작업이 디스크 I/O 한계에 도달하지 않도록 작업을 배치하거나 우선순위를 설정하여 대역폭을 관리할 수 있다.

3. 캐싱과 비동기 파일 처리의 상호작용

파일을 비동기적으로 읽고 쓸 때, 운영체제의 파일 캐시가 성능에 중요한 영향을 미친다. 대용량 파일을 처리할 때 파일 캐시가 효율적으로 동작하지 않으면, 파일을 매번 디스크에서 읽어야 하므로 I/O 성능이 크게 저하될 수 있다.

파일 캐시는 파일 시스템에서 파일을 메모리에 캐시하여, 동일한 파일에 대한 반복적인 접근이 있을 경우 디스크 I/O를 줄여 성능을 향상시킨다. 하지만 대용량 파일의 경우, 캐시의 크기가 부족할 수 있으며, 이로 인해 캐시 미스(cache miss)가 빈번하게 발생할 수 있다.

해결책: 캐시 효율을 높이기 위해서는 파일을 적절히 분할하고, 불필요한 캐시 사용을 피하는 전략이 필요하다. 예를 들어, 일괄적으로 처리해야 하는 대용량 파일은 캐시가 아니라 직접 디스크에서 데이터를 읽도록 설정할 수 있다. 이때 posix_fadvise와 같은 함수를 사용하여 운영체제에 파일 캐싱 정책을 제어하는 방법이 있다.

4. 비동기 작업의 순서 보장 문제

비동기 작업에서는 작업이 순차적으로 완료되지 않는 경우가 발생할 수 있다. 이는 특히 파일을 순차적으로 처리해야 할 때 문제가 된다. 예를 들어, 파일의 특정 순서대로 데이터를 처리해야 하는 상황에서 비동기 작업이 순서를 보장하지 않으면, 데이터 무결성이 손상될 수 있다.

S(t) = \{ A_1(t), A_2(t), \dots, A_n(t) \}

여기서:

따라서, 순서가 보장되지 않으면 파일의 특정 섹션이 나중에 완료될 수 있으며, 이는 데이터 처리에서 큰 문제를 일으킬 수 있다.

해결책: 순차 처리가 필요한 작업에서는 비동기 작업의 순서를 강제할 필요가 있다. 이를 위해, 작업 간의 의존성을 관리하는 메커니즘을 도입할 수 있다. 예를 들어, 이전 작업이 완료된 후에만 다음 작업이 실행되도록 콜백 체인을 구성하는 방식이다. Boost.Asio에서는 async_read와 같은 비동기 함수 호출 시, 콜백 함수 내부에서 다음 작업을 호출하여 순차성을 보장할 수 있다.

boost::asio::async_read(file_descriptor, boost::asio::buffer(data),
    [&](boost::system::error_code ec, std::size_t bytes_transferred) {
        if (!ec) {
            // 데이터를 처리한 후, 다음 작업을 호출
            boost::asio::async_write(another_descriptor, boost::asio::buffer(data), ...);
        }
    });

5. 오류 처리 및 복구

비동기 작업에서 오류 처리는 동기 작업에 비해 더 복잡하다. 비동기 파일 작업 중에 발생할 수 있는 오류는 파일의 존재 여부, 읽기/쓰기 권한, 디스크 공간 부족 등 다양한 이유로 발생할 수 있다. 이러한 오류들은 즉시 확인되지 않으며, 콜백 함수가 호출될 때야 비로소 감지된다.

해결책: 비동기 작업에서 오류를 처리하려면, 오류 코드나 예외를 콜백 함수 내에서 확인하고, 적절한 복구 조치를 취하는 로직을 추가해야 한다. Boost.Asio에서는 비동기 작업이 완료될 때 boost::system::error_code를 사용하여 오류 상태를 확인할 수 있다. 오류 발생 시 재시도(retry) 로직을 추가하거나, 특정 오류에 대해 사용자에게 알림을 보내는 방식으로 오류를 처리할 수 있다.

boost::asio::async_read(file_descriptor, boost::asio::buffer(data),
    [&](boost::system::error_code ec, std::size_t bytes_transferred) {
        if (ec) {
            // 오류 발생 시 처리 로직
        } else {
            // 정상 처리
        }
    });

대용량 파일 처리에서 비동기성과 파이프라인 패턴

대용량 파일을 처리할 때 비동기 작업을 더욱 효율적으로 관리하기 위한 방법 중 하나는 파이프라인 패턴이다. 파이프라인 패턴은 작업을 여러 단계로 나누어, 각 단계가 독립적으로 비동기적으로 실행되도록 구성하는 방식이다. 이를 통해 각 작업이 순차적으로 완료되지 않더라도 전체적인 흐름을 제어할 수 있다.

파이프라인은 다음과 같은 단계들로 구성될 수 있다:

  1. 입력 단계 (Input Stage): 파일로부터 데이터를 비동기적으로 읽어들이는 단계이다.
  2. 처리 단계 (Processing Stage): 읽어들인 데이터를 비동기적으로 처리하는 단계이다. 이 단계에서 데이터는 원하는 형식으로 변환되거나, 분석될 수 있다.
  3. 출력 단계 (Output Stage): 처리된 데이터를 비동기적으로 파일이나 네트워크로 내보내는 단계이다.

이러한 파이프라인 구조에서는 각 단계가 독립적으로 비동기 작업을 수행하므로, 한 단계에서 병목이 발생하더라도 다른 단계가 계속해서 작업을 수행할 수 있다.

파이프라인에서의 비동기 작업 흐름

파이프라인 패턴을 적용하면, 각 단계의 비동기 작업이 연결되어 데이터를 처리하게 된다. 이를 수학적으로 표현하면, 각 단계에서 데이터를 처리하는 시간을 다음과 같이 정의할 수 있다.

T_{total} = T_{input} + T_{process} + T_{output}

여기서:

파이프라인 패턴에서는 각 단계가 독립적으로 비동기 작업을 수행하기 때문에, 각 단계의 시간이 서로 중첩될 수 있다. 이는 파이프라인을 병렬로 실행할 수 있게 만들어 전체 처리 시간을 줄이는 데 기여한다.

파이프라인 패턴의 구현

Boost.Asio를 사용하여 파이프라인 패턴을 구현할 때, 각 단계의 비동기 작업은 서로 다른 콜백 함수로 처리된다. 이를 통해 단계별로 독립적인 비동기 작업 흐름을 관리할 수 있다.

예를 들어, 파일로부터 데이터를 읽고 처리한 후, 그 데이터를 다시 파일에 쓰는 비동기 작업 파이프라인을 구축할 수 있다.

boost::asio::async_read(file_descriptor, boost::asio::buffer(input_buffer),
    [&](boost::system::error_code ec, std::size_t bytes_transferred) {
        if (!ec) {
            // 데이터를 처리 단계로 전달
            process_data(input_buffer, bytes_transferred);
        }
    });
void process_data(char* data, std::size_t size) {
    // 데이터를 처리한 후 출력 단계로 전달
    boost::asio::async_write(output_descriptor, boost::asio::buffer(data, size),
        [&](boost::system::error_code ec, std::size_t bytes_written) {
            if (!ec) {
                // 출력 완료 후, 다음 비동기 작업을 진행할 수 있음
            }
        });
}

파이프라인 패턴의 병렬 처리

파이프라인 패턴을 적용할 때, 각 단계가 독립적으로 비동기적으로 실행되기 때문에 자연스럽게 병렬 처리가 가능하다. 그러나 병렬 처리 시 주의해야 할 점은 데이터 일관성이다. 여러 비동기 작업이 동시에 실행될 경우, 작업들 간에 의존성이 있을 수 있으며, 이 의존성을 적절히 관리하지 않으면 데이터 손상이나 오류가 발생할 수 있다.

이를 방지하기 위해 각 단계에서 작업이 완료된 후에 다음 단계로 안전하게 데이터를 전달하는 메커니즘을 도입해야 한다. 이를 위해 future 객체나 promise를 사용하여 작업 간의 의존성을 관리할 수 있다.

Boost.Asio에서의 대용량 파일 스트리밍

대용량 파일을 처리할 때 비동기 스트리밍 방식도 효율적인 방법 중 하나이다. 스트리밍 방식은 파일을 한꺼번에 처리하는 것이 아니라, 데이터를 일정 크기로 나누어 비동기적으로 처리하는 방식이다. 이는 메모리 사용량을 줄이는 동시에 파일 처리의 유연성을 높일 수 있다.

비동기 스트리밍 모델

비동기 스트리밍 모델에서는 데이터를 일정한 크기의 청크(chunk)로 나누어 처리한다. 이러한 방식은 메모리를 절약하면서도 비동기 작업의 병렬성을 높이는 데 기여할 수 있다. 이를 수식으로 표현하면, 파일을 n개의 청크로 나누어 처리할 때의 총 처리 시간은 다음과 같다.

T_{total} = \sum_{i=1}^{n} T_{chunk_i}

여기서:

스트리밍 방식에서 각 청크는 비동기적으로 처리되며, 데이터가 처리되는 동안 CPU는 다른 청크를 처리하는 비동기 작업을 수행할 수 있다.

비동기 스트리밍 구현

Boost.Asio를 사용하여 비동기 스트리밍을 구현할 수 있다. 파일을 일정 크기의 청크로 나누어, 각 청크를 비동기적으로 읽고 처리한 후, 비동기적으로 출력하는 작업을 구성할 수 있다. 이 과정에서 파일의 어느 부분까지 처리가 완료되었는지 추적해야 하며, 이를 위해 오프셋(offset)을 사용한다.

void async_stream_file(boost::asio::posix::stream_descriptor& file_descriptor, 
                       std::size_t file_size, 
                       std::size_t chunk_size) {
    std::size_t offset = 0;
    char* buffer = new char[chunk_size];

    auto read_handler = [&](boost::system::error_code ec, std::size_t bytes_read) {
        if (!ec && bytes_read > 0) {
            // 데이터를 처리한 후 다음 청크를 비동기적으로 읽음
            process_chunk(buffer, bytes_read);
            offset += bytes_read;
            if (offset < file_size) {
                boost::asio::async_read_at(file_descriptor, offset, boost::asio::buffer(buffer, chunk_size), read_handler);
            }
        }
    };

    // 첫 번째 청크 비동기 읽기 시작
    boost::asio::async_read_at(file_descriptor, offset, boost::asio::buffer(buffer, chunk_size), read_handler);
}

이 코드에서는 파일을 일정 크기로 비동기적으로 읽어들이고, 각 청크가 처리된 후 다음 청크를 읽는 방식으로 구현된다.

에러 복구와 재시도 메커니즘

비동기 스트리밍 방식에서도 파일 입출력 도중 발생할 수 있는 오류를 처리하는 것이 중요하다. 대용량 파일을 처리할 때 네트워크 장애나 디스크 오류 등의 문제가 발생할 수 있으며, 이때 각 청크의 처리에 실패할 수 있다.

이러한 오류가 발생하면 오류 처리 루틴을 추가하여 해당 청크의 처리를 재시도하거나, 적절한 복구 방법을 적용해야 한다. Boost.Asio에서는 비동기 작업의 결과로 발생한 오류를 콜백 함수 내부에서 처리할 수 있으며, 필요한 경우 해당 작업을 재시도하는 로직을 추가할 수 있다.

void async_stream_file_with_retry(boost::asio::posix::stream_descriptor& file_descriptor, 
                                  std::size_t file_size, 
                                  std::size_t chunk_size, 
                                  int max_retries) {
    std::size_t offset = 0;
    char* buffer = new char[chunk_size];
    int retry_count = 0;

    auto read_handler = [&](boost::system::error_code ec, std::size_t bytes_read) mutable {
        if (!ec && bytes_read > 0) {
            // 데이터 처리
            process_chunk(buffer, bytes_read);
            offset += bytes_read;
            if (offset < file_size) {
                retry_count = 0;  // 성공 시 재시도 카운터 초기화
                boost::asio::async_read_at(file_descriptor, offset, boost::asio::buffer(buffer, chunk_size), read_handler);
            }
        } else if (retry_count < max_retries) {
            // 오류 발생 시 재시도
            ++retry_count;
            boost::asio::async_read_at(file_descriptor, offset, boost::asio::buffer(buffer, chunk_size), read_handler);
        } else {
            // 재시도 실패 시 오류 처리
            handle_error(ec);
        }
    };

    boost::asio::async_read_at(file_descriptor, offset, boost::asio::buffer(buffer, chunk_size), read_handler);
}

이 코드에서는 오류 발생 시 재시도 로직을 포함하여 스트리밍 작업을 안전하게 처리하는 방식이다.