[2026] C++ 파일 입출력 | ifstream·ofstream으로 파일 열기 실패 에러 처리까지
이 글의 핵심
C++ 파일 입출력: ifstream·ofstream으로 파일 열기 실패 에러 처리까지. 실무에서 겪은 문제·파일 스트림 기초.
들어가며: “파일이 안 열려요”
설정 파일을 못 읽어서 프로그램이 죽었다
게임 설정을 저장하는 기능을 만들고 있었습니다. 하지만 파일이 없거나 권한이 없을 때 프로그램이 크래시했습니다. 문제의 코드에서는 std::ifstream file(“settings.txt”)로 파일을 열기만 하고, 열기 성공 여부를 확인하지 않은 채 file >> key >> value로 읽습니다. 파일이 없거나 권한이 없으면 스트림(데이터를 순서대로 읽거나 쓰는 추상적인 흐름. 예를 들면 파일·키보드 입력을 같은 방식으로 다룸)의 fail 비트(스트림이 오류 상태일 때 설정되는 플래그)가 설정된 상태로 남고, key와 value에는 쓰레기 값이 들어갈 수 있어 applySettings에 잘못된 값이 전달되거나 접근 오류가 날 수 있습니다. 실무에서는 “파일이 없다”는 상황이 자주 발생하므로, is_open() 또는 if (!file)로 한 번 검사한 뒤 읽는 습관이 중요합니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
void loadSettings() {
std::ifstream file("settings.txt");
std::string key, value;
file >> key >> value; // ❌ 파일이 안 열렸는데 읽으려고 함
applySettings(key, value);
}
위 코드 설명: 파일 열기 성공 여부를 검사하지 않고 file >> key >> value를 하면, 파일이 없거나 열기 실패 시 fail 비트가 설정된 상태에서 읽기가 진행되어 key·value에 쓰레기 값이 들어갈 수 있습니다. applySettings에 잘못된 값이 전달되거나 크래시로 이어질 수 있으므로, is_open() 또는 !file로 한 번 확인한 뒤 읽어야 합니다. 원인:
- 파일 열기 실패 여부를 확인 안 함
- 파일이 없어도
file >> key는 실행됨 (쓰레기 값) - 에러 처리 없음
스트림은 열기 실패 시 fail 비트가 설정되므로, 읽기 전에
is_open()또는!file로 한 번 확인하는 습관이 중요합니다. 경로 오타, 권한, 디스크 부족 등은 실무에서 자주 나오므로, 파일 I/O 직후에 검사해 두면 디버깅이 훨씬 수월해집니다. 텍스트 모드 vs 바이너리 모드: Windows에서는 기본이 텍스트 모드라서\n이\r\n으로 변환될 수 있습니다. 바이너리 파일(이미지, 압축 등)을 다룰 때는std::ios::binary로 열어야 바이트가 바뀌지 않습니다. Linux/macOS에서는 텍스트/바이너리 구분이 없어서 동작이 같지만, 크로스 플랫폼 코드에서는 바이너리일 때 명시적으로binary플래그를 쓰는 편이 안전합니다. 경로 참고:"settings.txt"처럼 상대 경로는 실행 시 현재 작업 디렉토리 기준으로 해석됩니다. 다른 폴더에서 실행하면 파일을 못 찾을 수 있으므로, 배포 시에는 설정 파일 경로를 고정하거나 인자/환경 변수로 받는 방식을 고려하면 좋습니다. 해결 후: 다음은 cpp를 활용한 상세한 구현 코드입니다. 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
void loadSettings() {
std::ifstream file("settings.txt");
if (!file.is_open()) {
std::cerr << "Cannot open settings.txt\n";
useDefaultSettings();
return;
}
std::string key, value;
while (file >> key >> value) {
applySettings(key, value);
}
if (file.bad()) {
std::cerr << "Error reading file\n";
}
}
위 코드 설명: is_open()으로 열기 실패 시 에러 메시지를 출력하고 기본 설정으로 돌아갑니다. while (file >> key >> value)는 읽기가 성공하는 동안만 루프를 돌고, 파일 끝이나 오류 시 종료됩니다. 읽기 후 file.bad()로 심각한 오류 여부를 확인하는 패턴입니다.
추가 문제 시나리오: 대용량 로그 파일 처리
시나리오: 10GB 로그 파일을 한 번에 메모리로 읽으려다가 메모리 부족으로 프로세스가 종료됩니다.
원인: std::string content((std::istreambuf_iterator<char>(file)), ...)처럼 전체 파일을 string에 담으면, 파일 크기만큼 메모리가 할당됩니다. 10GB 파일은 10GB 메모리를 요구하므로 OOM(Out of Memory)이 발생할 수 있습니다.
해결: 대용량 파일은 한 줄씩 또는 청크 단위로 읽어 처리합니다. getline으로 줄 단위로 처리하거나, read()로 고정 크기 버퍼를 사용해 반복 읽는 방식이 안전합니다.
추가 문제 시나리오: Windows에서 바이너리 파일이 깨짐
시나리오: 이미지 파일을 복사하는데, Windows에서 복사된 파일이 손상됩니다.
원인: std::ifstream in("image.png")처럼 기본적으로 텍스트 모드로 열면, Windows에서 \n(0x0A)이 \r\n(0x0D 0x0A)으로 변환됩니다. 바이너리 파일의 바이트는 그대로 읽어야 하는데 변환이 일어나면 이미지가 깨집니다.
해결: std::ios::binary 플래그로 열어야 합니다.
std::ifstream in("image.png", std::ios::binary);
std::ofstream out("copy.png", std::ios::binary);
추가 문제 시나리오: 디스크 풀·파일 핸들 누수·인코딩
디스크 풀: file << data는 버퍼에만 쓰이고, flush()/close() 시점에 디스크에 반영됩니다. 디스크가 꽉 찼을 때 쓰기 실패가 발생하므로 flush() 후 !file로 검사합니다.
파일 핸들 누수: 스트림은 소멸 시 자동으로 닫히지만, 전역/멤버로 두고 수동 관리할 때 close()를 빼먹으면 누수가 발생합니다. RAII 래퍼나 스코프 내 선언을 사용합니다.
인코딩 불일치: C++ 스트림은 바이트만 다룹니다. UTF-8 파일을 CP949 콘솔에 출력하면 깨져 보입니다. 인코딩을 맞추거나 iconv·ICU로 변환합니다.
이 글을 읽으면:
- ifstream, ofstream, fstream의 차이를 이해할 수 있습니다.
- 파일 열기/닫기와 에러 처리를 할 수 있습니다.
- 텍스트 파일을 안전하게 읽고 쓸 수 있습니다.
- 실전에서 자주 겪는 파일 I/O 문제를 해결할 수 있습니다. 실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.
목차
- 파일 스트림 기초
- 파일 읽기 (ifstream)
- 파일 쓰기 (ofstream)
- 파일 읽기/쓰기 (fstream)
- 에러 처리와 상태 확인
- 자주 발생하는 문제
- 버퍼링과 flush
- 모범 사례
- 성능 최적화 팁
- 프로덕션 패턴
1. 파일 스트림 기초
파일 스트림 아키텍처
아래 코드는 mermaid를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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
위 다이어그램 설명: ifstream은 파일에서 프로그램으로 읽기만, ofstream은 프로그램에서 파일로 쓰기만, fstream은 읽기와 쓰기 모두 가능합니다. 스트림은 데이터를 순서대로 흐르게 하는 추상화입니다.
파일 I/O 시퀀스 다이어그램
다음은 mermaid를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
sequenceDiagram
participant P as 프로그램
participant S as 스트림(버퍼)
participant F as 파일
P->>S: open(path)
S->>F: open()
F-->>S: 성공/실패
S-->>P: is_open()
alt 읽기
P->>S: read() / getline()
S->>F: read(버퍼)
F-->>S: 데이터
S-->>P: 데이터
else 쓰기
P->>S: write() / <<
S->>S: 버퍼에 저장
P->>S: flush()
S->>F: write(버퍼)
F-->>S: 완료
end
P->>S: close()
S->>F: close()
위 다이어그램 설명: 파일 열기 → 읽기/쓰기 → 버퍼 동작 → flush 시 디스크 반영 → close까지의 흐름을 보여줍니다. 쓰기는 버퍼에 먼저 쌓였다가 flush/close 시점에 디스크에 반영됩니다.
한 번에 복사해 실행 가능한 예제
아래는 파일을 쓰고 다시 읽어서 출력하는 완전한 프로그램입니다. data.txt를 미리 만들 필요 없이, 복사해 붙여넣고 빌드·실행하면 됩니다.
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 복사해 붙여넣은 뒤: g++ -std=c++17 -o file_io file_io.cpp && ./file_io
#include <fstream>
#include <iostream>
#include <string>
int main() {
// 1) 파일에 쓰기
{
std::ofstream out("data.txt");
if (out) {
out << "hello 1\nworld 2\n";
}
}
// 2) 같은 파일 읽기
std::ifstream in("data.txt");
if (!in) {
std::cerr << "Cannot open data.txt\n";
return 1;
}
std::string line;
while (std::getline(in, line)) {
std::cout << line << "\n";
}
return 0;
}
위 코드 설명: ofstream으로 data.txt에 “hello 1\nworld 2\n”를 쓰고, 스코프를 벗어나면 스트림이 닫힙니다. 그 다음 ifstream으로 같은 파일을 열어, !in으로 열기 실패를 확인한 뒤 getline으로 한 줄씩 읽어 cout에 출력합니다. 파일 쓰기→읽기 흐름을 한 번에 보여주는 예제입니다.
실행 결과: data.txt 에 쓰인 대로 hello 1 과 world 2 가 각각 한 줄씩 출력됩니다.
세 가지 파일 스트림
아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <fstream>
// 읽기 전용
std::ifstream input("data.txt");
// 쓰기 전용
std::ofstream output("result.txt");
// 읽기/쓰기
std::fstream file("config.txt", std::ios::in | std::ios::out);
위 코드 설명: ifstream은 읽기 전용, ofstream은 쓰기 전용, fstream은 읽기와 쓰기 모두 가능합니다. fstream은 열기 모드를 명시할 때 in | out처럼 비트 OR로 지정합니다. 파일 경로만 주면 ifstream/ofstream은 각각 in/out이 기본입니다. 특징:
ifstream: input file stream (읽기)ofstream: output file stream (쓰기)fstream: file stream (읽기/쓰기)
파일 열기 모드
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
std::ios::in // 읽기 (기본: ifstream)
std::ios::out // 쓰기 (기본: ofstream)
std::ios::app // 추가 (파일 끝에 덧붙임)
std::ios::trunc // 기존 내용 삭제 (기본: ofstream)
std::ios::binary // 바이너리 모드
위 코드 설명: in은 읽기, out은 쓰기, app은 파일 끝에 추가(덮어쓰지 않음), trunc는 기존 내용 삭제 후 쓰기, binary는 바이트 변환 없이 그대로 입출력합니다. ofstream 기본은 out|trunc라 기존 파일을 비우고 씁니다. 예제: 아래 코드는 cpp를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// 파일 끝에 추가
std::ofstream log("app.log", std::ios::app);
log << "New log entry\n";
// 읽기+쓰기
std::fstream file("data.txt", std::ios::in | std::ios::out);
// 바이너리 쓰기
std::ofstream bin("data.bin", std::ios::binary);
위 코드 설명: ios::app으로 열면 기존 내용 뒤에 덧붙이고, in|out으로 fstream을 열면 읽기와 쓰기가 모두 가능합니다. binary로 열면 Windows에서도 \n이 \r\n으로 변환되지 않아 이미지·압축 등 바이너리 파일에 적합합니다.
2. 파일 읽기 (ifstream)
기본 읽기
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <fstream>
#include <iostream>
#include <string>
int main() {
std::ifstream file("data.txt");
if (!file.is_open()) {
std::cerr << "Cannot open file\n";
return 1;
}
std::string word;
while (file >> word) {
std::cout << word << "\n";
}
file.close(); // 자동으로 닫히지만 명시적으로 닫기 가능
}
위 코드 설명: ifstream으로 파일을 열고 is_open()으로 열기 실패를 확인합니다. file >> word는 공백으로 구분된 단어를 하나씩 읽고, 스트림이 유효한 동안만 루프가 돌아 파일 끝에서 자연스럽게 끝납니다. 소멸 시 스트림이 닫히지만 필요하면 close()를 명시할 수 있습니다.
참고: data.txt가 없으면 “Cannot open file”이 나옵니다. 위 한 번에 복사해 실행 가능한 예제로 먼저 파일을 만든 뒤 실행하면 됩니다.
한 줄씩 읽기
아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
std::ifstream file("log.txt");
std::string line;
while (std::getline(file, line)) {
std::cout << line << "\n";
}
위 코드 설명: getline(file, line)은 개행 문자까지 한 줄을 읽어 line에 넣고, 개행은 버립니다. while (getline(…))으로 파일 끝까지 한 줄씩 읽을 수 있어, 로그나 설정 파일처럼 줄 단위로 처리할 때 자주 씁니다.
전체 파일 읽기
아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <sstream>
std::ifstream file("data.txt");
// 방법 1: stringstream 사용
std::stringstream buffer;
buffer << file.rdbuf();
std::string content = buffer.str();
// 방법 2: iterator 사용
std::string content((std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>());
위 코드 설명: rdbuf()로 스트림 버퍼 전체를 stringstream에 넣으면 content에 한 번에 문자열로 담을 수 있습니다. istreambuf_iterator로 [시작, 끝) 범위를 초기화 리스트처럼 쓰면 같은 내용을 한 번에 string으로 만들 수 있습니다. 주의: 작은 파일에만 사용하세요. 대용량 파일은 메모리 부족을 유발합니다.
CSV(Comma-Separated Values, 쉼표 구분 값) 파일 파싱
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <fstream>
#include <sstream>
#include <string>
#include <vector>
struct Person { std::string name; int age; std::string city; };
std::vector<Person> loadCSV(const std::string& filename) {
std::vector<Person> people;
std::ifstream file(filename);
if (!file) return people;
std::string line;
std::getline(file, line); // 헤더 스킵
while (std::getline(file, line)) {
std::stringstream ss(line);
Person p;
std::getline(ss, p.name, ',');
ss >> p.age;
ss.ignore();
std::getline(ss, p.city);
people.push_back(p);
}
return people;
}
위 코드 설명: getline으로 한 줄씩 읽고, stringstream으로 쉼표 구분 파싱. getline(ss, p.name, ’,‘)로 name, ss >> p.age로 숫자, ignore() 후 getline으로 city를 읽습니다.
완전한 파일 복사 예제 (에러 처리 포함)
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <cerrno>
#include <cstring>
#include <fstream>
#include <iostream>
bool copyFile(const std::string& src, const std::string& dst) {
std::ifstream in(src, std::ios::binary);
if (!in) { std::cerr << "Cannot open: " << src << " - " << std::strerror(errno) << "\n"; return false; }
std::ofstream out(dst, std::ios::binary);
if (!out) { std::cerr << "Cannot create: " << 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로 디버깅 메시지 제공.
바이너리 I/O: read()와 write()
텍스트가 아닌 바이트 단위로 읽고 쓸 때는 read()와 write()를 사용합니다. 구조체 직렬화, 이미지 처리 등에 쓰입니다.
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <fstream>
#include <iostream>
struct Record { int id; double value; char name[32]; };
bool writeRecord(const std::string& path, const Record& rec) {
std::ofstream out(path, std::ios::binary | std::ios::app);
if (!out) return false;
out.write(reinterpret_cast<const char*>(&rec), sizeof(rec));
return out.good();
}
bool readRecord(const std::string& path, size_t index, Record& rec) {
std::ifstream in(path, std::ios::binary);
if (!in) return false;
in.seekg(index * sizeof(Record), std::ios::beg);
in.read(reinterpret_cast<char*>(&rec), sizeof(Record));
return in.gcount() == sizeof(Record);
}
int main() {
Record r = {1, 3.14, "test"};
if (!writeRecord("data.bin", r)) return 1;
Record rec;
if (readRecord("data.bin", 0, rec))
std::cout << rec.id << " " << rec.value << " " << rec.name << "\n";
return 0;
}
위 코드 설명: write()는 메모리 블록을 그대로 파일에 쓰고, read()는 파일에서 바이트를 읽어 메모리에 넣습니다. reinterpret_cast로 구조체를 char*로 변환. gcount()는 마지막 read()에서 실제 읽은 바이트 수. seekg()로 인덱스 위치로 이동합니다. 주의: 구조체에 std::string이나 포인터가 있으면 바이너리 직렬화가 안전하지 않습니다. #pragma pack이나 고정 크기 타입 사용을 권장합니다.
청크 단위 바이너리 읽기 (대용량 파일)
아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
std::ifstream in("large.bin", std::ios::binary);
const size_t CHUNK = 64 * 1024;
std::vector<char> buffer(CHUNK);
while (in.read(buffer.data(), CHUNK) || in.gcount() > 0) {
processChunk(buffer.data(), in.gcount()); // gcount() = 실제 읽은 바이트
}
위 코드 설명: 64KB씩 읽어 처리합니다. EOF에서 read()가 짧게 읽으면 gcount()가 실제 바이트 수를 반환하므로, 이 값을 사용해야 합니다.
완전한 바이너리 I/O 예제 (에러 처리·RAII·버퍼링 포함)
바이너리 파일 복사를 read/write로 청크 단위 처리하고, 에러 처리·RAII·flush 검사를 모두 포함한 예제입니다.
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <cerrno>
#include <cstring>
#include <fstream>
#include <iostream>
#include <vector>
bool copyFileBinary(const std::string& src, const std::string& dst) {
const size_t CHUNK = 64 * 1024;
std::vector<char> buffer(CHUNK);
std::ifstream in(src, std::ios::binary);
if (!in) {
std::cerr << "Cannot open: " << src << " - " << std::strerror(errno) << "\n";
return false;
}
std::ofstream out(dst, std::ios::binary);
if (!out) {
std::cerr << "Cannot create: " << dst << " - " << std::strerror(errno) << "\n";
return false;
}
while (in.read(buffer.data(), CHUNK) || in.gcount() > 0) {
out.write(buffer.data(), in.gcount());
if (!out) { std::cerr << "Write failed\n"; return false; }
}
if (in.bad()) { std::cerr << "Read error\n"; return false; }
out.flush();
if (!out) { std::cerr << "Flush failed (disk full?)\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 copyFileBinary(argv[1], argv[2]) ? 0 : 1;
}
위 코드 설명: read/write로 64KB 청크 복사, gcount()로 실제 읽은 바이트 사용, flush() 후 !out으로 디스크 풀 검사, errno/strerror로 에러 메시지. RAII로 스트림이 스코프를 벗어나면 자동 close됩니다.
3. 파일 쓰기 (ofstream)
기본 쓰기
아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
std::ofstream file("output.txt");
if (!file.is_open()) {
std::cerr << "Cannot create file\n";
return 1;
}
file << "Hello, World!\n";
file << "Number: " << 42 << "\n";
위 코드 설명: ofstream으로 파일을 열고 is_open()으로 생성(열기) 성공 여부를 확인합니다. cout처럼 <<로 문자열과 숫자를 쓸 수 있고, 스트림이 닫힐 때까지 쓴 내용이 파일에 반영됩니다. 파일이 이미 있으면 기본 모드(trunc)에서 내용이 지워진 뒤 덮어씁니다.
파일 끝에 추가
std::ofstream log("app.log", std::ios::app);
log << "[" << getCurrentTime() << "] ";
log << "Application started\n";
위 코드 설명: ios::app으로 열면 기존 파일 내용을 유지한 채 끝에만 덧붙입니다. 로그처럼 계속 추가만 할 때 사용하면 이전 로그가 지워지지 않습니다.
CSV(Comma-Separated Values) 파일 생성
다음은 cpp를 활용한 상세한 구현 코드입니다. 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
void saveCSV(const std::vector<Person>& people, const std::string& filename) {
std::ofstream file(filename);
if (!file.is_open()) {
std::cerr << "Cannot create file\n";
return;
}
// 헤더
file << "Name,Age,City\n";
// 데이터
for (const auto& p : people) {
file << p.name << "," << p.age << "," << p.city << "\n";
}
}
위 코드 설명: 헤더 줄(“Name,Age,City\n”)을 먼저 쓰고, 각 Person을 “이름,나이,도시\n” 형식으로 한 줄씩 씁니다. ofstream은 파일이 없으면 생성하고, 있으면 기본적으로 내용을 비운 뒤 씁니다. CSV를 쓰는 기본 패턴입니다.
포맷팅
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <iomanip>
std::ofstream file("report.txt");
// 정수 포맷
file << std::setw(10) << 123 << "\n"; // " 123"
file << std::setfill('0') << std::setw(5) << 42 << "\n"; // "00042"
// 실수 포맷
file << std::fixed << std::setprecision(2) << 3.14159 << "\n"; // "3.14"
file << std::scientific << 1234.5 << "\n"; // "1.23e+03"
// 정렬
file << std::left << std::setw(10) << "Name" << "Age\n";
file << std::left << std::setw(10) << "Alice" << 25 << "\n";
위 코드 설명: iomanip의 setw(n)은 최소 너비, setfill(c)는 빈 칸 채울 문자, setprecision(n)은 소수 자리 수입니다. fixed는 고정 소수점, scientific은 지수 표기입니다. left는 왼쪽 정렬이라 보고서·로그 포맷을 맞출 때 유용합니다.
4. 파일 읽기/쓰기 (fstream)
설정 파일 읽고 수정하기
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <fstream>
#include <map>
#include <string>
class ConfigFile {
std::map<std::string, std::string> data;
std::string filename;
public:
ConfigFile(const std::string& file) : filename(file) { load(); }
void load() {
std::ifstream f(filename);
for (std::string line; std::getline(f, line); ) {
size_t p = line.find('=');
if (p != std::string::npos) data[line.substr(0, p)] = line.substr(p + 1);
}
}
void save() {
std::ofstream f(filename);
for (const auto& [k, v] : data) f << k << "=" << v << "\n";
}
std::string get(const std::string& k) const {
auto it = data.find(k);
return it != data.end() ? it->second : "";
}
void set(const std::string& k, const std::string& v) { data[k] = v; }
};
위 코드 설명: load()에서 getline으로 key=value를 map에 넣고, save()에서 ofstream으로 파일에 씁니다. get/set으로 메모리상 설정을 읽고 바꾼 뒤 save()로 반영하는 설정 파일 패턴입니다.
5. 에러 처리와 상태 확인
에러 처리 흐름
아래 코드는 mermaid를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
flowchart TD
A[파일 열기] --> B{is_open?}
B -->|No| C[에러 처리/기본값]
B -->|Yes| D[읽기/쓰기]
D --> E{good?}
E -->|No| F{fail?}
F -->|Yes| G[형식 오류]
F -->|No| H{bad?}
H -->|Yes| I[심각한 오류]
H -->|No| J{eof?}
J -->|Yes| K[정상 종료]
위 다이어그램 설명: 파일 열기 후 is_open()으로 성공 여부를 확인합니다. 읽기/쓰기 중 good()이 false가 되면 fail(), bad(), eof()로 원인을 구분해 처리합니다.
파일 상태 확인
다음은 cpp를 활용한 상세한 구현 코드입니다. 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
std::ifstream file("data.txt");
// 파일이 열렸는지
if (!file.is_open()) {
std::cerr << "Cannot open file\n";
}
// 읽기 가능한지
if (file.good()) {
std::cout << "File is good\n";
}
// **EOF**(End Of File, 파일 끝) 도달했는지
if (file.eof()) {
std::cout << "Reached end of file\n";
}
// 읽기 실패했는지
if (file.fail()) {
std::cerr << "Read operation failed\n";
}
// 심각한 에러인지
if (file.bad()) {
std::cerr << "Critical error\n";
}
위 코드 설명: is_open()은 파일이 실제로 열렸는지, good()은 스트림이 정상인지, eof()는 파일 끝에 도달했는지, fail()은 읽기/쓰기 실패(형식 오류 등)인지, bad()는 복구 불가한 오류인지 확인합니다. 읽기 전후로 이 플래그들을 보면 원인 파악에 도움이 됩니다.
상태 플래그
| 함수 | 의미 |
|---|---|
good() | 모든 상태 OK |
eof() | 파일 끝 도달 |
fail() | 읽기/쓰기 실패 |
bad() | 심각한 에러 |
clear()로 상태 초기화
아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
std::ifstream file("data.txt");
int value;
file >> value; // 숫자가 아닌 문자가 있으면 fail 비트 설정
if (file.fail()) {
file.clear(); // fail 비트 초기화
file.ignore(256, '\n'); // 잘못된 줄 스킵
// 다음 줄부터 다시 읽기 가능
}
위 코드 설명: fail()이 설정되면 이후 읽기가 모두 실패합니다. clear()로 상태 플래그를 초기화한 뒤 ignore()로 잘못된 입력을 건너뛰면, 다음 줄부터 다시 읽을 수 있습니다.
안전한 파일 읽기 패턴
다음은 cpp를 활용한 상세한 구현 코드입니다. 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
bool readFile(const std::string& filename, std::vector<std::string>& lines) {
std::ifstream file(filename);
if (!file.is_open()) {
std::cerr << "Cannot open: " << filename << "\n";
return false;
}
std::string line;
while (std::getline(file, line)) {
lines.push_back(line);
}
if (file.bad()) {
std::cerr << "Error reading: " << filename << "\n";
return false;
}
return true;
}
위 코드 설명: 파일을 열고 열기 실패 시 false를 반환합니다. getline으로 한 줄씩 벡터에 넣고, 루프 후 bad()로 읽기 중 심각한 오류가 있었는지 확인해 실패 시 false를 반환합니다. 호출자가 열기·읽기 오류를 구분해 처리할 수 있는 안전한 패턴입니다.
RAII로 자동 닫기
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <fstream>
#include <stdexcept>
#include <string>
class FileGuard {
std::ofstream file;
public:
FileGuard(const std::string& filename) : file(filename) {
if (!file.is_open()) {
throw std::runtime_error("Cannot open file");
}
}
~FileGuard() {
if (file.is_open()) {
file.close();
}
}
std::ofstream& get() { return file; }
};
void writeLog(const std::string& message) {
FileGuard guard("app.log");
guard.get() << message << "\n";
// 자동으로 닫힘
}
위 코드 설명: FileGuard는 생성자에서 파일을 열고 실패 시 예외를 던지며, 소멸자에서 is_open()이면 close()를 호출합니다. writeLog에서 guard 스코프를 벗어나면 소멸자가 호출되어 파일이 닫히므로, 예외가 나도 파일이 열린 채로 남는 일을 막을 수 있는 RAII 패턴입니다.
6. 자주 발생하는 문제
문제 1: “파일을 찾을 수 없습니다” (경로 오류)
증상: is_open()이 false를 반환하지만, 파일은 실제로 존재합니다.
원인:
- 상대 경로는 실행 시 작업 디렉토리 기준입니다.
./myapp을 다른 폴더에서 실행하면data.txt를 찾지 못합니다. - 경로 구분자: Windows는
\, Linux/macOS는/. C++17std::filesystem::path를 쓰면 자동 처리됩니다. 해결: 아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <filesystem>
#include <fstream>
namespace fs = std::filesystem;
// 실행 파일 기준 상대 경로
fs::path exeDir = fs::current_path();
fs::path configPath = exeDir / "config" / "settings.txt";
std::ifstream file(configPath);
문제 2: Windows에서 바이너리 파일이 손상됨
증상: 이미지·ZIP 등을 복사했는데 복사본이 깨집니다.
원인: 텍스트 모드(기본)에서 \n ↔ \r\n 변환이 발생합니다.
해결:
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ❌ 잘못된 방법
std::ifstream in("image.png");
// ✅ 올바른 방법
std::ifstream in("image.png", std::ios::binary);
std::ofstream out("copy.png", std::ios::binary);
문제 3: getline이 마지막 빈 줄을 읽지 않음
증상: 파일 끝에 빈 줄이 있는데 getline 루프에서 무시됩니다.
원인: while (getline(file, line))은 EOF에 도달하면 false를 반환해 루프가 끝납니다. 마지막 줄이 개행으로 끝나면 그 줄은 읽히지만, 그 다음 getline 호출에서 EOF를 만나 false가 됩니다. 빈 줄 자체는 정상적으로 읽힙니다.
참고: “마지막 빈 줄이 무시된다”고 느끼는 경우는, Windows에서 \r\n으로 끝나는 줄을 읽을 때 \r이 line에 남아 있을 수 있습니다. 이때는 if (!line.empty() && line.back() == '\r') line.pop_back();으로 제거할 수 있습니다.
문제 4: 파일 쓰기 후 내용이 비어 있음
증상: file << "data" 후 파일을 열어보면 비어 있습니다.
원인: 스트림이 버퍼링합니다. close() 전에 프로그램이 비정상 종료되면 버퍼에 남은 데이터가 디스크에 쓰이지 않습니다.
해결:
file << "important data\n";
file.flush(); // 즉시 디스크에 반영 (선택)
// 또는 스코프를 벗어나 close()가 호출되면 자동 flush
문제 5: fstream으로 읽기 후 쓰기 시 위치 오류
증상: fstream으로 읽은 뒤 쓰기를 하면 예상한 위치가 아닙니다.
원인: 읽기/쓰기 후 파일 위치 지정자가 이동합니다. 읽기와 쓰기 모드 전환 시 seekg()/seekp()로 위치를 명시해야 합니다.
해결:
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
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";
문제 6: 권한 거부 (Permission denied)
증상: is_open()이 false이고, errno가 EACCES(13)입니다.
원인: 읽기 전용 파일을 쓰기 모드로 열거나, 디렉토리에 쓰기 권한이 없거나, 파일이 다른 프로세스에 의해 잠겨 있습니다.
해결:
아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <cerrno>
#include <cstring>
std::ofstream file("readonly.txt");
if (!file) {
if (errno == EACCES) {
std::cerr << "Permission denied\n";
} else {
std::cerr << std::strerror(errno) << "\n";
}
}
문제 7: read() 후 gcount()를 사용하지 않음
증상: read()로 읽은 뒤 실제 읽은 바이트 수를 모르고 sizeof만 믿어서, EOF에서 짧게 읽은 경우 잘못된 데이터를 처리합니다.
원인: read(buf, 1024)가 EOF에서 500바이트만 읽으면 gcount()는 500을 반환합니다. sizeof나 요청 크기를 그대로 쓰면 나머지 524바이트는 이전 버퍼 내용(쓰레기)입니다.
해결:
아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
std::ifstream in("data.bin", std::ios::binary);
char buf[1024];
while (in.read(buf, sizeof(buf)) || in.gcount() > 0) {
size_t bytes = in.gcount(); // 실제 읽은 바이트 수
processChunk(buf, bytes);
}
문제 8: fstream을 in|out으로 열었는데 파일이 없음
증상: std::fstream f("new.txt", std::ios::in | std::ios::out)로 열었는데 is_open()이 false입니다.
원인: in|out만 쓰면 기존 파일이 있어야 열립니다. 새 파일을 만들려면 out을 포함하되, 없을 때 생성하려면 in|out|trunc 또는 out|trunc 후 in을 추가하는 식으로 조합해야 합니다. 또는 in|out|app으로 끝에 추가 모드로 열 수 있습니다.
해결:
// 기존 파일이 없으면 생성 후 읽기/쓰기
std::fstream f("data.txt", std::ios::in | std::ios::out | std::ios::trunc);
// 또는 먼저 ofstream으로 생성한 뒤 fstream으로 열기
문제 9: 동일 파일을 읽기·쓰기 스트림으로 동시에 열기
증상: 같은 파일을 ifstream과 ofstream으로 동시에 열었는데, 한쪽에서 쓴 내용이 다른 쪽에서 바로 보이지 않습니다.
원인: 각 스트림은 자체 버퍼를 갖습니다. 한쪽에서 쓴 뒤 flush()/close()하지 않으면, 다른 쪽은 버퍼된 이전 내용을 읽을 수 있습니다. 또한 플랫폼에 따라 동시 열기 동작이 제한될 수 있습니다.
해결: 쓰기 완료 후 flush()·close()를 하고 읽기 스트림을 새로 열거나, fstream 하나로 seekg/seekp로 위치를 옮겨가며 사용합니다.
문제 10: 디스크 풀·구조체 패딩
디스크 풀: flush()/close() 시점에 실제 디스크 쓰기가 일어나며, 그때 ENOSPC 에러가 발생합니다. flush() 후 !file 검사를 하지 않으면 실패를 놓칩니다.
구조체 패딩: Windows에서 저장한 .bin을 Linux에서 읽으면 필드가 어긋날 수 있습니다. #pragma pack(1)로 패딩 제거하거나, int32_t 등 고정 크기 타입을 사용하세요. 바이너리 직렬화(#11-2) 참고.
7. 버퍼링과 flush
스트림 버퍼 동작
파일 스트림은 버퍼를 사용해 작은 쓰기를 모아서 한 번에 디스크에 반영합니다. 이렇게 하면 시스템 콜 횟수가 줄어 성능이 좋아지지만, 버퍼가 비워지기 전에 프로그램이 종료되면 데이터가 손실될 수 있습니다.
std::ofstream log("app.log");
log << "Important message\n"; // 아직 버퍼에만 있음
// 프로그램 크래시 시 위 내용이 파일에 안 써질 수 있음
flush()와 endl
아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 즉시 디스크에 반영
log << "Critical log\n";
log.flush();
// endl = '\n' + flush (매번 flush는 느릴 수 있음)
log << "Line with endl" << std::endl;
// 버퍼만 비우고 개행은 별도 (성능이 더 좋음)
log << "Line without endl\n";
// 로그 파일은 주기적으로 flush() 호출 권장
위 코드 설명: flush()는 버퍼 내용을 디스크에 즉시 반영합니다. std::endl은 개행 문자를 출력한 뒤 flush()를 호출합니다. 로그처럼 중요 데이터는 주기적으로 flush()를 호출해 크래시 시에도 최대한 보존합니다. 단, 매 줄마다 endl을 쓰면 성능이 떨어질 수 있어, 배치 단위로 flush()하는 편이 좋습니다.
버퍼 크기 설정 (pubsetbuf)
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <fstream>
const size_t BUF_SIZE = 256 * 1024; // 256KB
char inbuf[BUF_SIZE];
char outbuf[BUF_SIZE];
std::ifstream in;
std::ofstream out;
// open() 전에 pubsetbuf 호출해야 함
in.rdbuf()->pubsetbuf(inbuf, BUF_SIZE);
out.rdbuf()->pubsetbuf(outbuf, BUF_SIZE);
in.open("large.bin", std::ios::binary);
out.open("output.bin", std::ios::binary);
// 이제 읽기/쓰기 시 시스템 콜 횟수가 줄어듦
위 코드 설명: pubsetbuf()는 스트림의 내부 버퍼를 사용자 제공 버퍼로 교체합니다. 주의: open() 호출 전에 설정해야 하므로, 스트림을 기본 생성한 뒤 pubsetbuf를 호출하고 open()합니다. 대용량 순차 I/O에서 버퍼를 키우면 시스템 콜이 줄어 성능이 좋아질 수 있습니다.
8. 모범 사례
파일 I/O 체크리스트
| 항목 | 내용 |
|---|---|
| 열기 검사 | is_open() 또는 !file로 항상 확인 |
| 바이너리 | 이미지·ZIP·직렬화 데이터는 std::ios::binary |
| 경로 | std::filesystem::path로 크로스 플랫폼 처리 |
| 대용량 | 전체 파일을 메모리에 올리지 말고 줄/청크 단위 처리 |
| 에러 메시지 | errno/strerror로 디버깅 정보 제공 |
| RAII | 스코프 안에서 스트림 선언 또는 래퍼 사용 |
| flush | 중요 로그·설정은 주기적 flush() |
읽기/쓰기 전용 선택
- 읽기만 필요 →
std::ifstream - 쓰기만 필요 →
std::ofstream - 읽기+쓰기 필요 →
std::fstream(모드in|out명시)fstream은 읽기/쓰기 전환 시seekg/seekp위치 관리가 필요합니다. 단순 읽기나 쓰기만 할 때는ifstream/ofstream이 더 단순합니다.
예외 vs 에러 코드
예상 가능한 실패(파일 없음)는 bool load(path, out) 형태의 에러 코드, 예상치 못한 실패는 throw std::runtime_error(...)로 처리합니다. 프로젝트 컨벤션에 맞게 선택합니다.
스트림 선택 가이드
| 용도 | 권장 스트림 | 모드 |
|---|---|---|
| 설정/로그 읽기 | ifstream | 기본 또는 binary |
| 로그/데이터 쓰기 | ofstream | app(추가) 또는 trunc(덮어쓰기) |
| 설정 파일 읽기+쓰기 | fstream | in | out |
| 이미지·ZIP·직렬화 | ifstream/ofstream | binary 필수 |
| 대용량 순차 읽기 | ifstream | binary + 청크 단위 read() |
에러 처리 패턴
예상 가능한 실패(파일 없음)는 bool load(path, out) 형태의 에러 코드로, 예상치 못한 실패는 throw std::runtime_error(...)로 처리합니다. 프로젝트 컨벤션에 맞게 선택하세요.
9. 성능 최적화 팁
팁 1: 대용량 파일은 줄 단위 또는 청크로 처리
나쁜 예 (메모리 부족 위험):
std::string content((std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>());
좋은 예: 다음은 간단한 cpp 코드 예제입니다. 반복문으로 데이터를 처리합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
std::string line;
while (std::getline(file, line)) {
processLine(line); // 한 줄씩 처리
}
팁 2: 버퍼 크기 조정
기본 버퍼(보통 8KB)보다 큰 블록으로 읽을 때는 rdbuf()->pubsetbuf()로 버퍼를 늘리면 시스템 콜 횟수가 줄어듭니다.
다음은 간단한 cpp 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
const size_t BUFFER_SIZE = 64 * 1024; // 64KB
char buffer[BUFFER_SIZE];
std::ifstream file("large.bin", std::ios::binary);
file.rdbuf()->pubsetbuf(buffer, BUFFER_SIZE);
팁 3: 불필요한 seek 피하기
rdbuf()로 스트림 전체를 복사할 때는 순차 읽기가 가장 빠릅니다. 중간에 seekg()를 반복 호출하면 디스크 I/O가 증가합니다.
팁 4: C++17 std::filesystem으로 파일 존재 확인
ifstream으로 열어보기 전에 exists()로 확인하면, 존재하지 않는 파일에 대한 불필요한 시도를 줄일 수 있습니다. 다만 TOCTOU(Time-of-check to time-of-use) 경쟁 조건이 있을 수 있으므로, 최종적으로는 is_open() 검사가 필수입니다.
아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고, 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
#include <filesystem>
namespace fs = std::filesystem;
if (fs::exists(path) && fs::is_regular_file(path)) {
std::ifstream file(path);
// ...
}
팁 5: 로그 파일은 ios::app으로 열기
trunc 모드로 매번 새로 쓰면 매번 파일을 비우는 오버헤드가 있습니다. 로그처럼 추가만 할 때는 std::ios::app으로 열어 기존 내용을 유지하고 끝에만 덧붙입니다.
10. 프로덕션 패턴
패턴 1: 로그 파일 래퍼 (날짜·레벨 포함)
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <chrono>
#include <fstream>
#include <iomanip>
#include <sstream>
class Logger {
std::ofstream file;
public:
explicit Logger(const std::string& path) : file(path, std::ios::app) {}
void log(const std::string& level, const std::string& msg) {
if (file.is_open()) {
auto t = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
file << "[" << std::put_time(std::localtime(&t), "%Y-%m-%d %H:%M:%S") << "] ["
<< level << "] " << msg << "\n";
file.flush(); // 크래시 시에도 최대한 로그 보존
}
}
};
패턴 2: 원자적 파일 쓰기 (임시 파일 + rename)
쓰기 중 크래시가 나도 기존 파일이 깨지지 않도록, 임시 파일에 쓰고 성공 시 rename합니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <filesystem>
#include <fstream>
#include <string>
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;
fs::rename(tmp, p); // 원자적 교체 (같은 파일시스템 내)
return true;
}
패턴 3: 예외 기반 에러 처리
아래 코드는 cpp를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
std::string readFileOrThrow(const std::string& path) {
std::ifstream file(path);
if (!file) throw std::runtime_error("Cannot open: " + path);
std::stringstream buffer;
buffer << file.rdbuf();
if (!file) throw std::runtime_error("Read failed: " + path);
return buffer.str();
}
패턴 4: 파일 존재 및 타입 확인 (C++17)
fs::exists(path)와 fs::is_regular_file(path)로 디렉토리 등을 제외한 뒤 ifstream으로 열고, rdbuf()로 읽은 뒤 !file로 읽기 실패를 검사합니다.
패턴 5: 스레드 안전 로그 (mutex)
std::mutex와 lock_guard로 여러 스레드의 동시 쓰기를 직렬화하고, flush()로 크래시 시에도 로그를 보존합니다.
패턴 6: 대용량 파일 원자적 쓰기 (청크 + rename)
대용량 데이터도 임시 파일에 쓰고 성공 시 rename하면, 쓰기 중 크래시가 나도 원본이 깨지지 않습니다. 패턴 2(원자적 파일 쓰기)와 동일한 원리로, 청크 단위 write()로 반복 쓰기 후 flush()·close() 검사 후 rename합니다.
프로덕션 체크리스트
- 모든 파일 열기 후
is_open()또는!file검사 - 바이너리 파일은
std::ios::binary사용 - 대용량 파일은 줄/청크 단위 처리
- 로그는
flush()또는std::endl로 버퍼 비우기 - 중요 설정/데이터는 원자적 쓰기(임시 파일 + rename) 고려
- 경로는
std::filesystem::path로 크로스 플랫폼 처리 - errno/strerror로 시스템 에러 메시지 로깅
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ 바이너리 직렬화 | “게임 세이브 파일 깨졌어요” 엔디안·패딩 문제 해결
- C++ stringstream | 문자열 파싱·변환·포맷팅
- C++ LNK2019 | “unresolved external symbol” 링커 에러 원인 5가지와 해결법
이 글에서 다루는 키워드 (관련 검색어)
C++ 파일 입출력, ifstream ofstream, fstream, 파일 읽기 쓰기, 텍스트 파일 바이너리 등으로 검색하시면 이 글이 도움이 됩니다.
정리
| 항목 | 내용 |
|---|---|
| ifstream | 읽기 전용 |
| ofstream | 쓰기 전용 |
| fstream | 읽기/쓰기 |
| is_open() | 파일 열림 확인 |
| good() | 상태 OK |
| eof() | 파일 끝 |
| fail() | 읽기/쓰기 실패 |
| getline() | 한 줄 읽기 |
| 핵심 원칙: |
- 항상
is_open()확인 - 에러 상태 체크
- RAII로 자동 닫기
- 예외 처리 고려
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. C++ 파일 입출력(File I/O) 완벽 가이드. ifstream·ofstream·fstream 사용법, 텍스트·바이너리 모드, 파일 열기 실패 에러 처리, getline·read·write, 파일 존재 확인, 실… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.