기타 Hacker News · 17시간 전

개발자들이 매일 쓰는 '읽기'와 '쓰기'가 사실 전혀 짝이 맞지 않는 이유

핵심 요약
  • POSIX의 read/write나 C의 fread/fwrite는 겉보기에 완벽한 대칭형 함수처럼 보이지만 내부 동작은 완전히 다릅니다.
  • 읽기는 에러 발생 시 즉각 감지되어 반환되지만, 다중 버퍼링을 거치는 쓰기 작업은 실제 디스크 반영 시점이 달라 비대칭성이 발생합니다.
  • 블록 장치 구조와 시스템 콜 오버헤드를 줄이기 위한 다계층 캐시 때문에 두 작업의 상태 변화가 근본적으로 차이납니다.
요약 컴퓨터 프로그래밍에서 파일이나 저장 장치를 다룰 때 사용하는 읽기(read)와 쓰기(write)는 언뜻 보기에 대칭적인 쌍둥이 작업처럼 보입니다. POSIX 시스템 콜의 read()와 write(), C 언어 표준 라이브러리의 fread()와 fwrite()처럼 함수 시그니처마저 거의 똑같기 때문입니다. 하지만 실제 내부 동작과 에러 처리 방식을 들여다보면 둘 사이에는 엄청난 비대칭성이 존재합니다. 가장 결정적인 차이는 '버퍼링(Buffering)' 레이어에서 비롯됩니다. 프로그램은 보통 저장 장치와 직접 통신하지 않고 커널의 버퍼 캐시와 유저 영역의 I/O 버퍼 등 여러 겹의 버퍼를 거칩니다. 디스크 같은 블록 장치는 512바이트나 4KB 단위의 고정된 블록으로만 데이터를 처리할 수 있기 때문에, 임의 바이트 단위의 요청을 변환해 주는 버퍼 캐시가 필수적입니다. 또한 유저 공간과 커널 공간을 오가는 시스템 콜 비용(최소 수백 클록 사이클)을 아끼기 위해 유저 영역 라이브러리(FILE*, Java BufferedReader 등) 역시 보통 4KB나 8KB 크기의 자체 버퍼를 운용합니다. 이때 '읽기'는 동기적이고 예측 가능합니다. 예를 들어 프로그램이 파일에서 100바이트를 읽으려(fread) 할 때 버퍼가 비어 있다면, 표준 라이브러리는 4KB 크기의 읽기 시스템 콜을 호출하고 커널은 디스크에서 데이터를 가져와 버퍼 캐시와 유저 버퍼를 채운 뒤 요청한 100바이트를 앱에 넘겨줍니다. 파일이 없거나 디스크 에러가 발생하는 등 문제가 생기면 그 에러는 즉시 감지되어 fread 호출이 끝나기 전에 앱으로 반환됩니다. 즉, 읽기 작업에서는 모든 실패가 호출 시점에 즉각 확정됩니다. 반면 '쓰기'는 완전히 다른 양상으로 흘러갑니다. 100바이트를 쓰려고 할 때 I/O 라이브러리는 그 데이터를 유저 공간 버퍼에 복사해 두기만 하고 즉시 성공으로 반환할 수 있기 때문에, 실제 디스크에 기록되는 과정에서 발생하는 에러는 호출 시점과 분리되어 개발자들을 곤혹스럽게 만듭니다.
Sponsored · 광고