스트림과 파일 입출력 개요
스트림을 통한 데이터 흐름과 파일의 지속성을 이해하고 열기·읽기·쓰기·상태 확인·닫기의 기본 절차를 익힙니다.
지금까지 우리는 키보드로부터 데이터를 입력받고 (std::cin), 화면에 데이터를 출력하는 (std::cout) 콘솔 입출력을 주로 사용해 왔습니다.
하지만 프로그램의 데이터는 일회성으로 사용되는 경우가 많습니다.
프로그램이 종료되면 입력받은 데이터나 처리된 결과는 모두 사라집니다.
실제 응용 프로그램에서는 데이터를 영구적으로 저장하거나, 이전에 저장된 데이터를 불러와서 사용해야 할 필요가 있습니다.
이때 사용되는 것이 바로 파일 입출력(File I/O)입니다.
이 장에서는 C++에서 파일 입출력을 처리하는 기본 개념인 스트림(Stream)과 파일 입출력의 전체 구조를 정리합니다.
C++ · stream architecture · persistence
스트림 API는 버퍼를 거쳐 외부 자원과 값을 주고받는다
형식화는 스트림이, 실제 전송은 연결된 std::streambuf가 맡습니다. 파일은 표준 구성이고 네트워크는 별도 라이브러리나 플랫폼의 버퍼와 어댑터가 필요합니다.
외부에서 프로그램으로
원천 → 버퍼·어댑터 → std::istream → 프로그램 순서로 읽습니다. 연결 상태와 읽기 결과를 각각 확인합니다.
프로그램에서 외부로
프로그램 → std::ostream → 버퍼·어댑터 → 파일 순서로 씁니다. 모드와 쓰기, 플러시 상태를 확인합니다.
양방향 파일 스트림
std::fstream은 std::iostream 계열 API와 파일 버퍼를 결합합니다. 방향과 모드, 읽기와 쓰기 위치를 함께 관리합니다.
네트워크는 별도 어댑터
표준 iostream에는 네트워크 스트림이 없습니다. 라이브러리나 플랫폼의 std::streambuf 구현이 필요합니다.
| 클래스 | 형식화 API | 데이터 방향 | 확인 책임 |
|---|---|---|---|
std::ifstream | std::istream | 파일 → 프로그램 | 연결 상태와 읽기 결과 |
std::ofstream | std::ostream | 프로그램 → 파일 | 모드와 쓰기·플러시 상태 |
std::fstream | std::iostream | 파일 ↔ 프로그램 | 방향·모드와 읽기/쓰기 위치 |
파일은 실행이 끝나도 값을 남기는 지속 장치입니다. 클래스를 고른 뒤에는 경로·모드·상태와 작업 결과를 확인합니다. 같은 API를 다른 자원에 재사용하려면 그 자원과 통신하는 버퍼나 어댑터가 있어야 합니다.
데이터 지속성 (Data Persistence)의 필요성
우리가 개발하는 프로그램은 종종 사용자 설정, 게임 진행 상황, 문서 내용, 이미지, 데이터베이스 정보 등 다양한 종류의 데이터를 다룹니다.
이러한 데이터가 프로그램 실행 중에만 존재하고 종료 시 사라진다면, 사용자는 매번 데이터를 다시 입력하거나 생성해야 하는 불편함을 겪게 됩니다.
데이터 지속성(Data Persistence)은 프로그램이 종료되더라도 데이터가 영구적으로 저장되어 다음 프로그램 실행 시에도 접근할 수 있도록 하는 능력입니다.
파일은 데이터를 지속적으로 저장하는 가장 기본적인 방법 중 하나입니다.
스트림 (Stream) 개념 이해하기
C++의 입출력은 스트림(Stream)이라는 추상적인 개념을 기반으로 합니다.
스트림은 데이터가 흐르는 통로(flow)를 의미합니다.
물이 흐르는 강물처럼 데이터도 한 곳에서 다른 곳으로 흘러간다고 생각할 수 있습니다.
- 출력 스트림 (Output Stream): 데이터가 프로그램에서 외부(화면, 파일 등)로 흘러나가는 통로입니다.
std::cout이 대표적인 출력 스트림입니다. - 입력 스트림 (Input Stream): 데이터가 외부(키보드, 파일 등)에서 프로그램으로 흘러들어오는 통로입니다.
std::cin이 대표적인 입력 스트림입니다.
C++ 표준 라이브러리(iostream 및 fstream 헤더)는 이러한 스트림을 처리하기 위한 클래스들을 제공합니다.
std::istream: 모든 입력 스트림의 기본 클래스입니다.std::cin이 이 클래스의 객체입니다.std::ostream: 모든 출력 스트림의 기본 클래스입니다.std::cout,std::cerr,std::clog가 이 클래스의 객체입니다.std::fstream: 파일 입출력을 위한 스트림 클래스입니다.
스트림 개념 덕분에 프로그래머는 형식화 인터페이스(<<, >>, getline 등)를 파일과 콘솔에 일관되게 사용할 수 있습니다.
이 인터페이스를 네트워크에 적용하려면 네트워크와 통신하는 std::streambuf 구현이나 라이브러리·플랫폼 어댑터가 필요합니다. C++ 표준 라이브러리가 네트워크 스트림 자체를 제공하는 것은 아닙니다.
예를 들어, 화면에 출력하는 것과 파일에 출력하는 것은 사용법이 거의 유사합니다.
파일 입출력의 기본 단계
스트림과 파일 입출력은 열기 모드, 상태 플래그, 읽기·쓰기 단위, close 책임으로 나누어 확인합니다.
C++ · file I/O · decision flow
방향을 먼저 고르고, 연결과 작업 상태를 따로 확인한다
클래스의 기본 방향과 명시한 openmode는 같은 규칙이 아닙니다. 특히 std::fstream에 모드를 넘기면 그 조합을 그대로 사용하므로 app만으로 in | out이 생기지 않습니다.
- 방향 선택
읽기는
ifstream, 쓰기는ofstream, 양방향은fstream을 고릅니다. - 모드 명시
fstream추가 쓰기는out | app, 양방향 추가는in | out | app처럼 의도를 모두 씁니다. - 연결 확인
open()뒤is_open()으로 파일 연관 여부를 봅니다. - 작업 결과로 반복
추출·
getline결과를 조건으로 사용하고 실패 이유는 상태 비트로 해석합니다. - RAII 정리
보통 소멸자가 닫습니다. 조기 해제나 닫기 오류 확인이 필요할 때만
close()를 명시합니다.
클래스마다 모드 합성 규칙이 다르다
ifstream은 in, ofstream은 out을 합칩니다. fstream의 in | out은 모드를 생략했을 때의 기본값이며, 넘긴 모드에는 자동으로 붙지 않습니다.
연결, 정상 상태, EOF를 구분한다
is_open()은 파일 연관만, if (file)은 현재 failbit·badbit 부재를 봅니다. eof()는 eofbit 보고값이므로 읽기 루프 조건을 대신하지 않습니다.
>> 뒤 남은 줄바꿈을 처리한다
in >> number;
std::getline(in >> std::ws, line);
std::ws는 줄바꿈을 포함한 선행 공백을 제거하므로 그 동작이 의도에 맞을 때 사용합니다.
정상 경로는 소멸자에 맡긴다
파일 스트림의 수명을 스코프로 제한하면 소멸 시 버퍼가 닫힙니다. 명시적 close()는 더 이른 경계나 오류 확인이라는 이유가 있을 때 사용합니다.
std::ofstream(path)의 기본 인수는 out입니다. 그 기본 호출의 out 단독 조합은 파일 버퍼에서 기존 내용을 비우는 쓰기 모드로 매핑되므로 이어 쓰기는 app을 명시합니다.
스트림 객체를 고를 때는 데이터의 방향과 파일을 다루는 의도를 먼저 분리해서 보면 실수가 줄어듭니다.
C++에서 파일을 사용하여 데이터를 저장하고 불러오는 과정은 다음과 같은 기본 단계를 따릅니다.
헤더 파일 포함: 파일 입출력을 위해 <fstream> 헤더 파일을 포함해야 합니다.
이 헤더는 파일 스트림 클래스들(std::ifstream, std::ofstream, std::fstream)을 정의합니다.
#include <fstream> // 파일 입출력 클래스들을 위해
#include <limits> // ignore에 사용할 최대 스트림 크기를 위해파일 스트림 객체 생성: 파일에서 읽거나 파일에 쓸 std::ifstream (입력 파일 스트림), std::ofstream (출력 파일 스트림), 또는 std::fstream (입력/출력 파일 스트림) 객체를 생성합니다.
// 출력 파일 스트림 객체 (파일에 쓸 때)
std::ofstream outputFile;
// 입력 파일 스트림 객체 (파일에서 읽을 때)
std::ifstream inputFile;
// 입출력 파일 스트림 객체 (읽고 쓸 때)
std::fstream ioFile;파일 열기 (Open): 생성된 스트림 객체를 특정 파일과 연결합니다.
파일 이름과 (필요하다면) 파일 열기 모드를 지정합니다.
outputFile.open("example.txt"); // 기본 인수 out, 기존 내용은 비움
inputFile.open("data.txt"); // 기본 인수 in
ioFile.open(
"log.txt",
std::ios::out | std::ios::app
); // fstream은 의도한 방향까지 명시파일을 열 때는 다음의 std::ios_base::openmode 플래그를 조합하여 열기 모드를 지정할 수 있습니다.
| 모드 플래그 | 방향과 동작 |
|---|---|
std::ios::in | 입력 시퀀스를 허용합니다. |
std::ios::out | 출력 시퀀스를 허용합니다. |
std::ios::app | 매 쓰기 전에 파일 끝으로 이동해 이어 씁니다. |
std::ios::ate | 연 직후 위치를 파일 끝으로 옮기되 이후 위치 변경은 허용합니다. |
std::ios::trunc | 파일을 열 때 기존 내용을 버립니다. |
std::ios::binary | 구현의 텍스트 변환 없이 이진 모드로 엽니다. |
std::ifstream의 생성자와open은 기본 인수가in이며, 전달한 모드에in을 합쳐 파일 버퍼를 엽니다.std::ofstream은 기본 인수가out이며, 전달한 모드에out을 합칩니다. 기본 호출로 만들어지는out단독 조합은 파일 버퍼에서 기존 내용을 비우는 쓰기 모드에 매핑되므로 이어 쓰려면app을 명시합니다.std::fstream은 모드를 생략했을 때만in | out이 기본입니다. 모드를 직접 넘기면 그 값이 그대로 전달되므로app만 넘겨도in | out을 상속하지 않습니다. 출력 추가는out | app, 양방향 추가는in | out | app처럼 의도를 모두 씁니다.
바이너리 모드 실습은 다음 절 ‘바이너리 파일 입출력’에서 별도로 다룹니다.
파일 연결 확인: is_open()은 스트림이 파일과 연결되었는지만 확인합니다.
파일이 없거나 접근 권한이 없는 경우 열기 작업이 실패할 수 있습니다.
if (outputFile.is_open()) {
// 파일 열기 성공
} else {
// 파일 열기 실패
std::cerr << "파일을 열 수 없습니다!" << std::endl;
}if (outputFile)은 열기 여부와 같은 질문이 아니라 현재 failbit·badbit가 없는지를 묻습니다. 작업 뒤 상태를 확인할 때 사용합니다. eof()는 eofbit가 설정됐다는 보고이므로, 읽기 루프는 while (std::getline(inputFile, line))처럼 작업 자체의 성공 결과를 조건으로 삼습니다.
파일 읽기/쓰기: 스트림 연산자(<<, >>)나 멤버 함수(read, write, getline 등)를 사용하여 파일에 데이터를 쓰거나 파일에서 데이터를 읽습니다.
// 파일에 쓰기 (출력 스트림)
outputFile << "Hello, File I/O!" << std::endl;
outputFile << 123 << " " << 45.67 << std::endl;
// 파일에서 읽기 (입력 스트림)
std::string line;
int number;
double value;
if (inputFile >> number >> value) { // 형식화 입력
inputFile.ignore(
std::numeric_limits<std::streamsize>::max(),
'\n'
); // 남은 줄바꿈 제거
std::getline(inputFile, line); // 다음 한 줄 읽기
}형식화 입력 >>는 구분 문자를 스트림에 남길 수 있습니다. 이어서 getline을 호출할 때는 위처럼 줄바꿈을 ignore하거나, 선행 공백 제거가 의도에 맞다면 std::getline(inputFile >> std::ws, line)을 사용합니다.
수명과 닫기 (RAII): 파일 스트림 객체가 스코프를 벗어나 소멸되면 연결된 파일 버퍼도 닫힙니다.
정상 경로에서는 이 RAII 수명에 맡기는 편이 누락을 막습니다. 파일 연결을 더 일찍 끝내거나 close() 실패를 확인해야 할 때는 명시적으로 닫습니다.
outputFile.close();
inputFile.close();스트림 객체가 스코프를 벗어나 소멸될 때 (예: 함수 종료 시 지역 변수인 경우), 자동으로 close()가 호출됩니다.
간단한 파일 쓰기 예제
#include <iostream> // 콘솔 출력을 위해
#include <fstream> // 파일 입출력을 위해
#include <string> // std::string을 위해
int main() {
// 1. 출력 파일 스트림 객체 생성
std::ofstream outFile;
// 2. 파일 열기 (파일이 없으면 생성, 있으면 내용 지움)
outFile.open("my_output.txt"); // 기본적으로 std::ios::out 모드
// 3. 파일 열기 성공 여부 확인
if (outFile.is_open()) {
// 4. 파일에 데이터 쓰기
outFile << "이것은 파일에 쓰여질 첫 번째 줄입니다.\n";
outFile << "숫자: " << 12345 << std::endl;
outFile << "이름: " << "홍길동" << std::endl;
std::cout << "파일 'my_output.txt'에 성공적으로 기록했습니다." << std::endl;
// 이 예제에서는 스코프가 끝날 때 RAII로 닫힌다.
} else {
std::cerr << "오류: 'my_output.txt' 파일을 열 수 없습니다!" << std::endl;
}
return 0;
}위 코드를 실행하면, 프로세스의 현재 작업 디렉터리에 my_output.txt라는 파일이 생성되거나 기존 파일의 내용이 지워지고 새로운 내용이 기록됩니다.
간단한 파일 읽기 예제
파일 입출력은 방향 선택, 모드 지정, 연결 확인, 작업 결과 확인, RAII 정리의 책임으로 나누어 볼 수 있습니다.
#include <iostream> // 콘솔 출력을 위해
#include <fstream> // 파일 입출력을 위해
#include <string> // std::string을 위해
int main() {
// 1. 입력 파일 스트림 객체 생성
std::ifstream inFile;
// 2. 파일 열기
inFile.open("my_output.txt"); // 위에서 생성한 파일을 읽기 모드로 엶
// 3. 파일 열기 성공 여부 확인
if (inFile.is_open()) {
// 4. 파일에서 데이터 읽기
std::string line;
// 파일 끝까지 한 줄씩 읽기
while (std::getline(inFile, line)) { // inFile에서 한 줄을 읽어 line 변수에 저장
std::cout << line << std::endl;
}
if (inFile.bad()) {
std::cerr << "파일을 읽는 중 I/O 오류가 발생했습니다." << std::endl;
return 1;
}
std::cout << "\n파일 'my_output.txt'에서 성공적으로 읽었습니다." << std::endl;
// 이 예제에서는 스코프가 끝날 때 RAII로 닫힌다.
} else {
std::cerr << "오류: 'my_output.txt' 파일을 열 수 없습니다! (파일이 없거나 권한 문제)" << std::endl;
}
return 0;
}위 코드를 실행하면, my_output.txt 파일의 내용을 한 줄씩 읽어 콘솔에 출력합니다.
파일 입출력 코드는 스트림 종류보다 먼저 “어떤 방향으로 열고, 열기 성공을 확인했는가”를 점검하면 예외 상황을 훨씬 차분하게 다룰 수 있습니다.