C++ 파일 연산 완벽 가이드 | ifstream·바이너리 I/O·mmap·io_uring·원자적 쓰기까지

C++ 파일 연산 완벽 가이드 | ifstream·바이너리 I/O·mmap·io_uring·원자적 쓰기까지

이 글의 핵심

C++ 파일 연산 완벽 가이드에 대한 실전 가이드입니다. ifstream·바이너리 I/O·mmap·io_uring·원자적 쓰기까지 등을 예제와 함께 설명합니다.

들어가며: “파일 쓰다가 크래시했는데 데이터가 다 날아갔어요”

설정 파일 저장 중 비정상 종료로 손실

게임 설정을 저장하는 기능을 만들었습니다. file << "resolution=1920x1080"로 쓰고 있었는데, 저장 중 프로그램이 크래시했습니다. 다시 실행해 보니 기존 설정이 통째로 사라지고 빈 파일만 남아 있었습니다.

원인: std::ofstream은 기본적으로 기존 파일을 trunc(비우기) 한 뒤 쓰기 시작합니다. 쓰기 도중 크래시하면 새 내용이 디스크에 완전히 반영되지 않은 상태에서 기존 파일은 이미 비워진 상태입니다. 원자적 쓰기(임시 파일에 쓰고 성공 시 rename)를 쓰지 않으면 이런 문제가 발생합니다.

flowchart LR
  subgraph bad["❌ 직접 덮어쓰기"]
    B1[기존 파일 trunc] --> B2[쓰기 중...]
    B2 --> B3[크래시]
    B3 --> B4[데이터 손실]
  end
  subgraph good["✅ 원자적 쓰기"]
    G1[임시 파일에 쓰기] --> G2[완료 시 rename]
    G2 --> G3[기존 파일 보존 또는 원자적 교체]
  end

이 글을 읽으면:

  • ifstream/ofstream으로 텍스트·바이너리 파일을 안전하게 다룰 수 있습니다.
  • mmap으로 대용량 파일을 효율적으로 읽을 수 있습니다.
  • io_uring으로 비동기 고성능 I/O를 구현할 수 있습니다.
  • 원자적 쓰기로 크래시 시에도 데이터 손실을 방지할 수 있습니다.
  • 자주 겪는 에러와 프로덕션 패턴을 적용할 수 있습니다.

개념을 잡는 비유

시간·파일·로그·JSON은 도구 상자의 자주 쓰는 렌치입니다. 표준·검증된 라이브러리로 한 가지 규칙을 정해 두면, 팀 전체가 같은 단위·같은 포맷으로 맞출 수 있습니다.


목차

  1. 문제 시나리오
  2. ifstream/ofstream 완전 예제
  3. 바이너리 I/O
  4. mmap 메모리 매핑
  5. 원자적 쓰기
  6. io_uring 비동기 I/O
  7. 자주 발생하는 에러와 해결법
  8. 모범 사례
  9. 프로덕션 패턴
  10. I/O 방식 비교와 선택 가이드
  11. 체크리스트
  12. 정리

1. 문제 시나리오

시나리오 1: 대용량 로그 파일 처리 시 메모리 부족

문제: 10GB 로그 파일을 std::string content((std::istreambuf_iterator<char>(file)), ...)로 한 번에 읽으려다 OOM(Out of Memory) 으로 프로세스가 종료됩니다.

원인: 전체 파일을 메모리에 올리면 파일 크기만큼 메모리가 필요합니다.

해결: 줄 단위 또는 청크 단위로 읽기. 또는 mmap으로 파일을 매핑해 필요한 부분만 접근.

시나리오 2: Windows에서 이미지 복사 시 파일 손상

문제: std::ifstream in("image.png")로 이미지를 읽어 복사했는데, Windows에서 복사본이 깨집니다.

원인: 기본 텍스트 모드에서 \n(0x0A)이 \r\n(0x0D 0x0A)으로 변환됩니다. 바이너리 파일은 변환 없이 그대로 읽어야 합니다.

해결: std::ios::binary 플래그로 열기.

시나리오 3: 설정 파일 저장 중 크래시로 기존 데이터 손실

문제: 설정 파일을 덮어쓰다 크래시하면 기존 설정이 모두 사라집니다.

원인: ofstream 기본 모드(out|trunc)는 기존 파일을 비운 뒤 쓰기 시작합니다.

해결: 임시 파일에 쓰고 성공 시 rename()으로 원자적 교체.

시나리오 4: 수천 개 파일을 순차적으로 읽을 때 지연

문제: 디렉토리에 수천 개 파일이 있고, 하나씩 read()로 읽으면 전체 처리 시간이 길어집니다.

원인: 동기 I/O는 한 번에 하나의 요청만 처리합니다. 시스템 콜 오버헤드가 누적됩니다.

해결: io_uring으로 여러 파일을 비동기 배치 읽기.

시나리오 5: 구조체를 그대로 저장했다가 다른 플랫폼에서 깨짐

문제: file.write(reinterpret_cast<const char*>(&data), sizeof(data))로 구조체를 저장했는데, 다른 CPU·OS에서 읽으면 데이터가 깨집니다.

원인: 패딩·엔디안 차이. 구조체 메모리 덤프는 이식성이 없습니다.

해결: 필드 단위 직렬화, 고정 크기 타입(uint32_t 등), 버전·길이 정보 포함.


2. ifstream/ofstream 완전 예제

파일 스트림 아키텍처

flowchart TB
  subgraph input["입력"]
    I1[파일] --> I2[ifstream]
    I2 --> I3[프로그램]
  end
  subgraph output["출력"]
    O1[프로그램] --> O2[ofstream]
    O2 --> O3[파일]
  end
  subgraph bidirectional["양방향"]
    B1[파일] <--> B2[fstream]
    B2 <--> B3[프로그램]
  end

기본 읽기/쓰기 (한 번에 실행 가능)

// g++ -std=c++17 -o file_demo file_demo.cpp && ./file_demo
#include <fstream>
#include <iostream>
#include <string>

int main() {
    // 1) 파일에 쓰기
    {
        std::ofstream out("demo.txt");
        if (!out) {
            std::cerr << "Cannot create demo.txt\n";
            return 1;
        }
        out << "Hello, File I/O!\n";
        out << "Line 2\n";
    }  // 스코프 종료 시 자동 close

    // 2) 파일 읽기
    std::ifstream in("demo.txt");
    if (!in.is_open()) {
        std::cerr << "Cannot open demo.txt\n";
        return 1;
    }

    std::string line;
    while (std::getline(in, line)) {
        std::cout << line << "\n";
    }

    if (in.bad()) {
        std::cerr << "Read error\n";
        return 1;
    }
    return 0;
}

위 코드 설명: ofstream으로 demo.txt에 쓰고, 스코프를 벗어나면 자동으로 닫힙니다. ifstream으로 같은 파일을 열어 getline으로 한 줄씩 읽습니다. !in 또는 !in.is_open()으로 열기 실패를 반드시 확인합니다.

열기 모드

#include <fstream>

// 읽기 전용
std::ifstream in("data.txt");

// 쓰기 전용 (기존 내용 삭제)
std::ofstream out("output.txt");

// 파일 끝에 추가
std::ofstream log("app.log", std::ios::app);

// 바이너리 모드 (이미지·압축 등)
std::ifstream bin_in("image.png", std::ios::binary);
std::ofstream bin_out("copy.png", std::ios::binary);

// 읽기+쓰기
std::fstream file("config.txt", std::ios::in | std::ios::out);

위 코드 설명: std::ios::in은 읽기, out은 쓰기, app은 추가, binary는 바이트 변환 없이 그대로 입출력. fstream은 in | out으로 양방향을 지정합니다.

에러 처리 포함 파일 복사

#include <cerrno>
#include <cstring>
#include <fstream>
#include <iostream>
#include <string>

bool copyFile(const std::string& src, const std::string& dst) {
    std::ifstream in(src, std::ios::binary);
    if (!in) {
        std::cerr << "Cannot open source: " << src
                  << " - " << std::strerror(errno) << "\n";
        return false;
    }

    std::ofstream out(dst, std::ios::binary);
    if (!out) {
        std::cerr << "Cannot create destination: " << dst
                  << " - " << std::strerror(errno) << "\n";
        return false;
    }

    out << in.rdbuf();

    if (!out) {
        std::cerr << "Write failed\n";
        return false;
    }
    return true;
}

int main(int argc, char* argv[]) {
    if (argc != 3) {
        std::cerr << "Usage: " << argv[0] << " <source> <destination>\n";
        return 1;
    }
    return copyFile(argv[1], argv[2]) ? 0 : 1;
}

위 코드 설명: rdbuf()로 입력 스트림 전체를 출력 스트림으로 복사합니다. binary 모드로 열어 Windows에서도 바이너리 파일이 깨지지 않습니다. errnostrerror로 시스템 에러 메시지를 출력합니다.


3. 바이너리 I/O

read/write 기본

텍스트 모드에서는 <<로 숫자가 문자열로 변환됩니다. 바이너리 모드에서는 read/write로 메모리 내용을 그대로 입출력합니다.

#include <fstream>
#include <vector>
#include <cstdint>

// 바이너리 쓰기
void writeBinary(const std::string& path, const std::vector<uint32_t>& data) {
    std::ofstream out(path, std::ios::binary);
    if (!out) return;

    for (uint32_t v : data) {
        out.write(reinterpret_cast<const char*>(&v), sizeof(v));
    }
}

// 바이너리 읽기
std::vector<uint32_t> readBinary(const std::string& path) {
    std::ifstream in(path, std::ios::binary);
    if (!in) return {};

    std::vector<uint32_t> result;
    uint32_t v;
    while (in.read(reinterpret_cast<char*>(&v), sizeof(v))) {
        result.push_back(v);
    }
    return result;
}

위 코드 설명: uint32_t처럼 고정 크기 타입을 사용하면 플랫폼 간 크기가 일정합니다. reinterpret_cast로 메모리 내용을 char*로 해석해 read/write합니다.

구조체 직렬화 (버전·길이 포함)

#include <fstream>
#include <string>
#include <cstdint>

struct Config {
    uint32_t version = 1;
    uint32_t width;
    uint32_t height;
    std::string title;

    void save(const std::string& path) const {
        std::ofstream out(path, std::ios::binary);
        if (!out) return;

        out.write(reinterpret_cast<const char*>(&version), sizeof(version));
        out.write(reinterpret_cast<const char*>(&width), sizeof(width));
        out.write(reinterpret_cast<const char*>(&height), sizeof(height));

        uint32_t len = static_cast<uint32_t>(title.size());
        out.write(reinterpret_cast<const char*>(&len), sizeof(len));
        out.write(title.data(), len);
    }

    bool load(const std::string& path) {
        std::ifstream in(path, std::ios::binary);
        if (!in) return false;

        in.read(reinterpret_cast<char*>(&version), sizeof(version));
        in.read(reinterpret_cast<char*>(&width), sizeof(width));
        in.read(reinterpret_cast<char*>(&height), sizeof(height));

        uint32_t len;
        in.read(reinterpret_cast<char*>(&len), sizeof(len));
        if (!in || len > 1024 * 1024) return false;  // 상한 검사

        title.resize(len);
        in.read(&title[0], len);
        return static_cast<bool>(in);
    }
};

위 코드 설명: 버전 번호를 먼저 저장해 포맷 호환성을 관리합니다. 가변 길이 문자열은 “길이(uint32_t) + 바이트” 순서로 저장합니다. len에 상한을 두어 악의적인 파일로부터 보호합니다.

청크 단위 대용량 읽기

#include <fstream>
#include <vector>
#include <functional>
#include <cstddef>

bool processFileByChunks(const std::string& path, size_t chunk_size,
                         std::function<void(const char*, size_t)> processor) {
    std::ifstream in(path, std::ios::binary);
    if (!in) return false;

    std::vector<char> buffer(chunk_size);
    while (in.read(buffer.data(), chunk_size) || in.gcount() > 0) {
        processor(buffer.data(), static_cast<size_t>(in.gcount()));
    }
    return !in.bad();
}

// 사용 예
int main() {
    processFileByChunks("large.bin", 64 * 1024,  {
        // 64KB씩 처리
    });
    return 0;
}

위 코드 설명: read는 요청한 바이트 수만큼 읽지 못할 수 있습니다. gcount()로 실제 읽은 바이트 수를 확인합니다. 마지막 청크는 chunk_size보다 작을 수 있으므로 in.gcount() > 0 조건으로 처리합니다.


4. mmap 메모리 매핑

mmap이란?

mmap은 파일을 프로세스의 가상 메모리 공간에 직접 매핑하는 Linux/Unix 시스템 콜입니다. read() 대신 페이지 폴트를 통해 필요할 때만 디스크에서 로드하므로, 대용량 파일 순차 읽기에서 시스템 콜 횟수를 크게 줄입니다.

flowchart LR
  subgraph read_way["read() 방식"]
    R1[read 호출] --> R2[커널 버퍼 복사]
    R2 --> R3[사용자 버퍼]
  end
  subgraph mmap_way["mmap 방식"]
    M1[mmap 호출] --> M2[가상 주소 매핑]
    M2 --> M3[포인터로 직접 접근]
  end

mmap 읽기 (RAII 래퍼)

// g++ -std=c++17 -o mmap_read mmap_read.cpp
// Linux/macOS 전용
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include <cstring>
#include <stdexcept>
#include <string>

class MmapFile {
public:
    explicit MmapFile(const char* path) {
        fd_ = open(path, O_RDONLY);
        if (fd_ < 0) {
            throw std::runtime_error(std::string("open failed: ") + path);
        }

        struct stat st;
        if (fstat(fd_, &st) < 0) {
            close(fd_);
            fd_ = -1;
            throw std::runtime_error("fstat failed");
        }
        size_ = static_cast<size_t>(st.st_size);

        if (size_ == 0) {
            data_ = nullptr;
            return;
        }

        data_ = static_cast<const char*>(
            mmap(nullptr, size_, PROT_READ, MAP_PRIVATE, fd_, 0));
        if (data_ == MAP_FAILED) {
            close(fd_);
            fd_ = -1;
            throw std::runtime_error("mmap failed");
        }

        // 순차 읽기 힌트: 커널이 read-ahead 수행
        madvise(const_cast<char*>(data_), size_, MADV_SEQUENTIAL);
    }

    ~MmapFile() {
        if (data_ && data_ != MAP_FAILED) {
            munmap(const_cast<char*>(data_), size_);
        }
        if (fd_ >= 0) close(fd_);
    }

    const char* data() const { return data_; }
    size_t size() const { return size_; }

    MmapFile(const MmapFile&) = delete;
    MmapFile& operator=(const MmapFile&) = delete;

private:
    int fd_ = -1;
    size_t size_ = 0;
    const char* data_ = nullptr;
};

int main(int argc, char* argv[]) {
    if (argc < 2) return 1;
    MmapFile f(argv[1]);
    size_t lines = 0;
    for (size_t i = 0; i < f.size(); ++i) {
        if (f.data()[i] == '\n') ++lines;
    }
    return 0;
}

위 코드 설명: MAP_PRIVATE는 쓰기 시 copy-on-write. 읽기 전용이면 PROT_READ만으로 충분합니다. MADV_SEQUENTIAL로 커널에 순차 접근을 알려 read-ahead를 유도합니다. 소멸자에서 munmapclose를 호출해 리소스를 해제합니다.

mmap 쓰기

#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include <cstring>

void writeFileMmap(const char* path, const void* data, size_t len) {
    int fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 0644);
    if (fd < 0) return;

    if (ftruncate(fd, static_cast<off_t>(len)) < 0) {
        close(fd);
        return;
    }

    void* addr = mmap(nullptr, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (addr == MAP_FAILED) {
        close(fd);
        return;
    }

    memcpy(addr, data, len);
    msync(addr, len, MS_SYNC);  // 디스크에 반영
    munmap(addr, len);
    close(fd);
}

위 코드 설명: MAP_SHARED로 매핑하면 수정 내용이 파일에 반영됩니다. msync(MS_SYNC)로 디스크에 동기화합니다. munmap 시 자동 flush되지만, 크래시 대비해 명시적 sync를 권장합니다.

mmap 주의사항

  • 파일 크기 제한: 32비트에서는 2GB 근처 제한. 64비트에서는 대용량 가능.
  • 동시 수정: MAP_SHARED로 매핑한 파일을 다른 프로세스가 수정하면 undefined behavior.
  • SIGBUS: mmap한 파일이 truncate되면 접근 시 SIGBUS. 수정 가능한 파일에는 주의.
  • 반드시 munmap: RAII로 감싸서 해제를 보장할 것.

5. 원자적 쓰기

임시 파일 + rename 패턴

쓰기 중 크래시가 나도 기존 파일이 깨지지 않도록, 임시 파일에 쓰고 성공 시 rename으로 원자적 교체합니다. rename은 같은 파일시스템 내에서 원자적입니다.

#include <filesystem>
#include <fstream>
#include <string>
#include <system_error>

namespace fs = std::filesystem;

bool atomicWrite(const std::string& path, const std::string& content) {
    fs::path p(path);
    fs::path tmp = p.parent_path() / (p.filename().string() + ".tmp");

    std::ofstream out(tmp, std::ios::binary);
    if (!out) return false;

    out << content;
    out.close();
    if (!out) return false;

    try {
        fs::rename(tmp, p);  // 원자적 교체 (같은 파일시스템)
    } catch (const std::filesystem_error&) {
        fs::remove(tmp);
        return false;
    }
    return true;
}

위 코드 설명: 임시 파일(.tmp)에 먼저 쓰고, close() 후 스트림 상태를 확인합니다. rename이 실패하면 임시 파일을 삭제하고 false를 반환합니다. rename은 같은 파일시스템 내에서 원자적으로 동작합니다.

바이너리 원자적 쓰기

#include <filesystem>
#include <fstream>
#include <vector>
#include <cstddef>

namespace fs = std::filesystem;

bool atomicWriteBinary(const std::string& path,
                       const void* data, size_t size) {
    fs::path p(path);
    fs::path tmp = p.parent_path() / (p.filename().string() + ".tmp");

    std::ofstream out(tmp, std::ios::binary);
    if (!out) return false;

    out.write(static_cast<const char*>(data), size);
    out.close();
    if (!out) return false;

    try {
        fs::rename(tmp, p);
    } catch (const std::filesystem_error&) {
        fs::remove(tmp);
        return false;
    }
    return true;
}

// 사용 예: vector 저장
bool saveVector(const std::string& path, const std::vector<int>& vec) {
    return atomicWriteBinary(path, vec.data(),
                             vec.size() * sizeof(int));
}

위 코드 설명: 바이너리 데이터도 같은 패턴으로 임시 파일에 쓰고 rename합니다. fsync가 필요하면 out.flush() 후 플랫폼별 fsync 호출을 추가할 수 있습니다.

fsync: 전원 장애 시 데이터 보존이 중요하면, out.flush()fsync(fd)로 커널 버퍼를 디스크에 반영할 수 있습니다.


6. io_uring 비동기 I/O

io_uring이란?

io_uring은 Linux 5.1+에서 도입된 비동기 I/O 인터페이스입니다. SQ(Submission Queue)에 I/O 요청을 넣고, CQ(Completion Queue)에서 완료 이벤트를 받습니다. 시스템 콜 횟수를 줄이고, 고성능 서버·DB에서 활용됩니다.

flowchart TB
  subgraph app["애플리케이션"]
    SQ[Submission Queue]
    CQ[Completion Queue]
  end
  subgraph kernel["커널"]
    K[io_uring]
  end
  SQ -->|제출| K
  K -->|완료| CQ

liburing 설치

# Ubuntu/Debian
sudo apt install liburing-dev

# 빌드
g++ -std=c++17 -O2 -o io_uring_demo io_uring_demo.cpp -luring

io_uring 기본 읽기

// g++ -std=c++17 -o io_uring_read io_uring_read.cpp -luring
#include <liburing.h>
#include <fcntl.h>
#include <cstdio>
#include <cstring>
#include <stdexcept>

int main(int argc, char* argv[]) {
    if (argc < 2) return 1;

    struct io_uring ring;
    if (io_uring_queue_init(32, &ring, 0) < 0) {
        throw std::runtime_error("io_uring_queue_init failed");
    }

    int fd = open(argv[1], O_RDONLY);
    if (fd < 0) {
        io_uring_queue_exit(&ring);
        throw std::runtime_error("open failed");
    }

    char buf[4096];
    struct io_uring_sqe* sqe = io_uring_get_sqe(&ring);
    io_uring_prep_read(sqe, fd, buf, sizeof(buf), 0);
    io_uring_sqe_set_data(sqe, buf);

    io_uring_submit(&ring);

    struct io_uring_cqe* cqe = nullptr;
    io_uring_wait_cqe(&ring, &cqe);
    int ret = cqe->res;
    io_uring_cqe_seen(&ring, cqe);

    if (ret > 0) {
        buf[ret] = '\0';
        printf("Read %d bytes: %s\n", ret, buf);
    } else if (ret < 0) {
        fprintf(stderr, "Read error: %s\n", strerror(-ret));
    }

    close(fd);
    io_uring_queue_exit(&ring);
    return 0;
}

위 코드 설명: io_uring_prep_read로 read 요청을 준비하고, io_uring_submit으로 커널에 제출합니다. io_uring_wait_cqe로 완료를 기다리고, cqe->res가 읽은 바이트 수(음수면 에러)입니다.

배치 읽기: 여러 파일에 대해 io_uring_prep_read로 SQE를 채우고 io_uring_submit으로 한 번에 제출한 뒤, io_uring_wait_cqe로 완료된 순서대로 처리합니다. io_uring_sqe_set_data64로 인덱스를 연결해 어떤 파일인지 식별합니다.

io_uring 쓰기

#include <liburing.h>
#include <fcntl.h>
#include <vector>
#include <cstring>

int writeFileUring(const char* path, const void* data, size_t len) {
    int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (fd < 0) return -1;

    struct io_uring ring;
    if (io_uring_queue_init(32, &ring, 0) < 0) {
        close(fd);
        return -1;
    }

    struct io_uring_sqe* sqe = io_uring_get_sqe(&ring);
    io_uring_prep_write(sqe, fd, data, len, 0);

    io_uring_submit(&ring);

    struct io_uring_cqe* cqe = nullptr;
    io_uring_wait_cqe(&ring, &cqe);
    int ret = cqe->res;
    io_uring_cqe_seen(&ring, cqe);

    io_uring_queue_exit(&ring);
    close(fd);
    return ret;
}

위 코드 설명: io_uring_prep_write로 쓰기 요청을 준비합니다. 대용량 쓰기는 청크로 나눠 여러 SQE에 넣고 배치 제출할 수 있습니다.


7. 자주 발생하는 에러와 해결법

문제 1: “파일을 찾을 수 없습니다” (경로 오류)

증상: is_open()이 false인데 파일은 존재합니다.

원인: 상대 경로는 실행 시 작업 디렉토리 기준입니다. 다른 폴더에서 실행하면 찾지 못합니다.

해결:

#include <filesystem>
#include <fstream>

namespace fs = std::filesystem;

fs::path configPath = fs::current_path() / "config" / "settings.txt";
std::ifstream file(configPath);

문제 2: Windows에서 바이너리 파일 손상

증상: 이미지·ZIP 복사 시 깨짐.

원인: 텍스트 모드에서 \n\r\n 변환.

해결:

// ❌ 잘못된 방법
std::ifstream in("image.png");

// ✅ 올바른 방법
std::ifstream in("image.png", std::ios::binary);
std::ofstream out("copy.png", std::ios::binary);

문제 3: 파일 쓰기 후 내용이 비어 있음

증상: file << "data" 후 파일을 열면 비어 있습니다.

원인: 버퍼링. close() 전에 비정상 종료하면 버퍼가 디스크에 쓰이지 않습니다.

해결:

file << "important data\n";
file.flush();  // 즉시 커널로 전송
// 또는 스코프를 벗어나 close()가 호출되면 자동 flush

문제 4: fstream 읽기 후 쓰기 위치 오류

증상: 읽은 뒤 쓰기를 하면 예상한 위치가 아닙니다.

원인: 읽기/쓰기 후 파일 위치 지정자가 이동합니다.

해결:

std::fstream file("data.txt", std::ios::in | std::ios::out);
std::string line;
std::getline(file, line);

file.seekp(0, std::ios::end);  // 쓰기 위치를 파일 끝으로
file << "appended\n";

문제 5: mmap SIGBUS / io_uring “Function not implemented”

mmap SIGBUS: 다른 프로세스가 파일을 truncate했을 때. mmap 후 파일 수정 금지, 로컬 파일시스템 사용.

io_uring 실패: Linux 5.1 미만. uname -r로 확인. WSL2 지원.

문제 6: 대용량 OOM / 권한 거부

OOM: istreambuf_iterator로 전체 읽기 금지. getline 또는 청크 read, mmap 사용.

권한 거부: errno EACCES. std::strerror(errno)로 메시지 출력. 읽기 전용 파일을 쓰기 모드로 열지 않기.


8. 모범 사례

  1. 항상 열기 검사: if (!file.is_open()) 또는 if (!file) 후 에러 처리
  2. 바이너리 파일: std::ios::binary 플래그 필수
  3. RAII: 스코프로 감싸 자동 close
  4. 대용량: getline 또는 read 청크 단위 — istreambuf_iterator로 전체 읽기 금지
  5. 중요 데이터: 임시 파일에 쓰고 rename으로 원자적 교체
  6. 디버깅: errno·strerror로 시스템 에러 메시지 출력
  7. 경로: std::filesystem::path로 크로스 플랫폼
  8. 가변 길이: “길이(uint32_t) + 데이터” 순서로 저장

9. 프로덕션 패턴

패턴 1: 로그 파일 래퍼 (날짜·레벨·flush)

#include <chrono>
#include <fstream>
#include <iomanip>
#include <sstream>
#include <string>

class FileLogger {
    std::ofstream file_;

    std::string timestamp() {
        auto now = std::chrono::system_clock::now();
        auto t = std::chrono::system_clock::to_time_t(now);
        std::ostringstream oss;
        oss << std::put_time(std::localtime(&t), "%Y-%m-%d %H:%M:%S");
        return oss.str();
    }

public:
    explicit FileLogger(const std::string& path)
        : file_(path, std::ios::app) {}

    void log(const std::string& level, const std::string& msg) {
        if (file_.is_open()) {
            file_ << "[" << timestamp() << "] [" << level << "] "
                  << msg << "\n";
            file_.flush();  // 크래시 시에도 최대한 보존
        }
    }
};

패턴 2: 설정 로드/저장 (원자적 쓰기)

#include <filesystem>
#include <fstream>
#include <map>
#include <string>

namespace fs = std::filesystem;

class Config {
    std::map<std::string, std::string> data_;
    fs::path path_;

public:
    explicit Config(const std::string& path) : path_(path) { load(); }

    void load() {
        std::ifstream in(path_);
        if (!in) return;
        std::string line;
        while (std::getline(in, line)) {
            auto pos = line.find('=');
            if (pos != std::string::npos) {
                data_[line.substr(0, pos)] = line.substr(pos + 1);
            }
        }
    }

    bool save() {
        fs::path tmp = path_;
        tmp += ".tmp";
        std::ofstream out(tmp);
        if (!out) return false;
        for (const auto& [k, v] : data_) {
            out << k << "=" << v << "\n";
        }
        out.close();
        if (!out) return false;
        fs::rename(tmp, path_);
        return true;
    }

    std::string get(const std::string& key) const {
        auto it = data_.find(key);
        return it != data_.end() ? it->second : "";
    }
    void set(const std::string& key, const std::string& value) {
        data_[key] = value;
    }
};

패턴 3: 파일 존재 및 타입 확인 (C++17)

#include <filesystem>
#include <fstream>

namespace fs = std::filesystem;

bool safeReadFile(const fs::path& path, std::string& out) {
    if (!fs::exists(path) || !fs::is_regular_file(path)) return false;
    std::ifstream file(path);
    if (!file) return false;
    std::stringstream buffer;
    buffer << file.rdbuf();
    if (!file) return false;
    out = buffer.str();
    return true;
}

10. I/O 방식 비교와 선택 가이드

방식별 비교

방식시스템 콜복사블로킹플랫폼용도
ifstream/ofstream호출당 1회2회동기모든일반 파일 I/O
read/write (POSIX)호출당 1회2회동기Unix저수준 제어
mmap페이지 폴트 시0~1회페이지 폴트 시Linux/macOS대용량 순차/랜덤
io_uring배치 제출2회비동기Linux 5.1+고성능 서버
splice/sendfile1회0회 (커널 내)동기Linux파일→소켓 전송

선택 가이드

flowchart TD
  A[파일 I/O 필요] --> B{용도?}
  B -->|설정·로그·일반| C[ifstream/ofstream]
  B -->|대용량 순차 읽기| D[mmap 또는 청크 read]
  B -->|비동기·다중 I/O| E[io_uring]
  B -->|크래시 안전 저장| F[원자적 쓰기]
  C --> G[완료]
  D --> G
  E --> G
  F --> G
  • 일반적인 읽기/쓰기: ifstream/ofstream
  • 대용량 파일 (수백MB~): mmap 또는 청크 단위 read
  • 수천 개 파일 동시 처리: io_uring
  • 설정·세이브 등 중요 데이터: 원자적 쓰기 (임시 + rename)
  • 바이너리 (이미지·직렬화): std::ios::binary 필수

11. 체크리스트

구현 체크리스트

  • 모든 파일 열기 후 is_open() 또는 !file 검사
  • 바이너리 파일은 std::ios::binary 사용
  • 대용량 파일은 줄/청크 단위 처리 (한 번에 메모리에 올리지 않기)
  • 로그는 flush()로 버퍼 비우기
  • 중요 설정/데이터는 원자적 쓰기 (임시 파일 + rename)
  • 경로는 std::filesystem::path로 크로스 플랫폼 처리
  • errno/strerror로 시스템 에러 메시지 로깅
  • mmap 사용 시 RAII로 munmap 보장
  • io_uring 사용 시 Linux 5.1+ 확인

프로덕션 배포 전

  • 에러 경로 테스트 (파일 없음, 권한 없음, 디스크 부족)
  • 대용량 파일로 메모리 사용량 확인
  • 크래시 시나리오에서 원자적 쓰기 동작 검증

12. 정리

항목내용
ifstream읽기 전용
ofstream쓰기 전용
binary바이너리 모드 (이미지·직렬화)
mmap대용량 파일 메모리 매핑
io_uringLinux 비동기 고성능 I/O
원자적 쓰기임시 파일 + rename

핵심 원칙:

  1. 항상 is_open() 확인
  2. 바이너리는 std::ios::binary
  3. 대용량은 청크/줄 단위
  4. 중요 데이터는 원자적 쓰기
  5. RAII로 리소스 해제

자주 묻는 질문 (FAQ)

Q. mmap vs io_uring 언제 뭘 쓰나요?

A. mmap은 순차·랜덤 읽기가 많은 대용량 파일에 적합합니다. io_uring은 비동기·다중 I/O, 이벤트 기반 서버에 적합합니다. Linux 5.1+ 필요.

Q. Windows에서 mmap·io_uring을 쓸 수 있나요?

A. mmap에 해당하는 Windows API는 CreateFileMapping/MapViewOfFile입니다. io_uring은 Linux 전용이며, Windows에서는 OVERLAPPED·IOCP를 사용합니다.

Q. 원자적 쓰기가 정말 필요한가요?

A. 설정 파일, 세이브 데이터, DB WAL처럼 “크래시 시 기존 데이터가 손실되면 안 되는” 경우에 필요합니다. 단순 로그는 ios::app으로 추가만 해도 됩니다.


한 줄 요약: ifstream/ofstream으로 기본 I/O를 하고, 바이너리는 binary 모드, 대용량은 mmap·청크, 중요 데이터는 원자적 쓰기를 적용하세요.

이전 글: C++23 핵심 기능 (#37-1)

다음 글: C++ 아키텍처: 클린 코드 (#38-1)


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • C++ 파일 입출력 | ifstream·ofstream으로 “파일 열기 실패” 에러 처리까지
  • C++ 바이너리 직렬화 | “게임 세이브 파일 깨졌어요” 엔디안·패딩 문제 해결
  • C++ 백준/프로그래머스 C++ 세팅과 입출력 최적화 한 번에 정리 [#32-1]

실전 체크리스트

실무에서 이 개념을 적용할 때 확인해야 할 사항입니다.

코드 작성 전

  • 이 기법이 현재 문제를 해결하는 최선의 방법인가?
  • 팀원들이 이 코드를 이해하고 유지보수할 수 있는가?
  • 성능 요구사항을 만족하는가?

코드 작성 중

  • 컴파일러 경고를 모두 해결했는가?
  • 엣지 케이스를 고려했는가?
  • 에러 처리가 적절한가?

코드 리뷰 시

  • 코드의 의도가 명확한가?
  • 테스트 케이스가 충분한가?
  • 문서화가 되어 있는가?

이 체크리스트를 활용하여 실수를 줄이고 코드 품질을 높이세요.


이 글에서 다루는 키워드 (관련 검색어)

C++ 파일 연산, ifstream ofstream, mmap, io_uring, 바이너리 I/O, 원자적 쓰기, 파일 입출력, 고성능 I/O 등으로 검색하시면 이 글이 도움이 됩니다.


관련 글

  • C++ 남들보다 먼저 써보는 C++23 핵심 기능 [#37-1]
  • C++ std::filesystem 완벽 가이드 | 경로·디렉토리·파일·권한 한 번에 정리
  • C++ 현대적인 C++ GUI: Dear ImGui로 디버깅 툴·대시보드 만들기 [#36-1]
  • C++ 크로스 플랫폼 GUI | Qt 기초 완벽 가이드 [#36-2]
  • [C++ 클린 코드 기초: const, noexcept, ]로 인터페이스 의도 명확히 하기](/blog/cpp-series-38-1-clean-code-const-noexcept-nodiscard/)