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은 도구 상자의 자주 쓰는 렌치입니다. 표준·검증된 라이브러리로 한 가지 규칙을 정해 두면, 팀 전체가 같은 단위·같은 포맷으로 맞출 수 있습니다.
목차
- 문제 시나리오
- ifstream/ofstream 완전 예제
- 바이너리 I/O
- mmap 메모리 매핑
- 원자적 쓰기
- io_uring 비동기 I/O
- 자주 발생하는 에러와 해결법
- 모범 사례
- 프로덕션 패턴
- I/O 방식 비교와 선택 가이드
- 체크리스트
- 정리
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에서도 바이너리 파일이 깨지지 않습니다. errno와 strerror로 시스템 에러 메시지를 출력합니다.
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를 유도합니다. 소멸자에서 munmap과 close를 호출해 리소스를 해제합니다.
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. 모범 사례
- 항상 열기 검사:
if (!file.is_open())또는if (!file)후 에러 처리 - 바이너리 파일:
std::ios::binary플래그 필수 - RAII: 스코프로 감싸 자동 close
- 대용량:
getline또는read청크 단위 —istreambuf_iterator로 전체 읽기 금지 - 중요 데이터: 임시 파일에 쓰고
rename으로 원자적 교체 - 디버깅:
errno·strerror로 시스템 에러 메시지 출력 - 경로:
std::filesystem::path로 크로스 플랫폼 - 가변 길이: “길이(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/sendfile | 1회 | 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_uring | Linux 비동기 고성능 I/O |
| 원자적 쓰기 | 임시 파일 + rename |
핵심 원칙:
- 항상
is_open()확인 - 바이너리는
std::ios::binary - 대용량은 청크/줄 단위
- 중요 데이터는 원자적 쓰기
- 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/)