스트림과 파일 입출력 개요
지금까지 우리는 키보드로부터 데이터를 입력받고 (std::cin), 화면에 데이터를 출력하는 (std::cout) 콘솔 입출력을 주로 사용해 왔습니다.
하지만 프로그램의 데이터는 일회성으로 사용되는 경우가 많습니다.
프로그램이 종료되면 입력받은 데이터나 처리된 결과는 모두 사라집니다.
실제 응용 프로그램에서는 데이터를 영구적으로 저장하거나, 이전에 저장된 데이터를 불러와서 사용해야 할 필요가 있습니다.
이때 사용되는 것이 바로 파일 입출력(File I/O)입니다.
이 장에서는 C++에서 파일 입출력을 처리하는 기본 개념인 스트림(Stream)과 파일 입출력의 전체 구조를 정리합니다.
데이터 지속성 (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: 파일 입출력을 위한 스트림 클래스입니다.
스트림 개념 덕분에 프로그래머는 데이터의 출처(키보드, 파일, 네트워크 등)나 목적지(화면, 파일, 네트워크 등)에 상관없이 동일한 인터페이스(<< 연산자, >> 연산자 등)를 사용하여 데이터를 처리할 수 있습니다.
예를 들어, 화면에 출력하는 것과 파일에 출력하는 것은 사용법이 거의 유사합니다.
파일 입출력의 기본 단계
스트림과 파일 입출력은 열기 모드, 상태 플래그, 읽기·쓰기 단위, close 책임으로 나누어 확인합니다.
스트림 객체를 고를 때는 데이터의 방향과 파일을 다루는 의도를 먼저 분리해서 보면 실수가 줄어듭니다.
C++에서 파일을 사용하여 데이터를 저장하고 불러오는 과정은 다음과 같은 기본 단계를 따릅니다.
헤더 파일 포함: 파일 입출력을 위해 <fstream> 헤더 파일을 포함해야 합니다. 이 헤더는 파일 스트림 클래스들(std::ifstream, std::ofstream, std::fstream)을 정의합니다.
#include <fstream> // 파일 입출력 클래스들을 위해파일 스트림 객체 생성: 파일에서 읽거나 파일에 쓸 std::ifstream (입력 파일 스트림), std::ofstream (출력 파일 스트림), 또는 std::fstream (입력/출력 파일 스트림) 객체를 생성합니다.
// 출력 파일 스트림 객체 (파일에 쓸 때)
std::ofstream outputFile;
// 입력 파일 스트림 객체 (파일에서 읽을 때)
std::ifstream inputFile;
// 입출력 파일 스트림 객체 (읽고 쓸 때)
std::fstream ioFile;파일 열기 (Open): 생성된 스트림 객체를 특정 파일과 연결합니다. 파일 이름과 (필요하다면) 파일 열기 모드를 지정합니다.
outputFile.open("example.txt"); // "example.txt" 파일을 쓰기 모드로 염
inputFile.open("data.txt"); // "data.txt" 파일을 읽기 모드로 염
ioFile.open("log.txt", std::ios::app); // "log.txt" 파일을 추가 모드로 염파일을 열 때는 다음의 std::ios_base::openmode 플래그를 조합하여 열기 모드를 지정할 수 있습니다.
| 모드 플래그 | 설명 | 기본값 |
|---|---|---|
std::ios::in | 읽기 모드로 파일 열기 | ifstream의 기본 |
std::ios::out | 쓰기 모드로 파일 열기 (파일이 없으면 생성, 있으면 내용 지움) | ofstream의 기본 |
std::ios::app | 쓰기 모드, 파일 끝에 데이터를 추가 (append) | |
std::ios::ate | 파일 열 때 파일 포인터를 파일 끝으로 이동 (at end) | |
std::ios::trunc | 파일 열 때 파일 내용을 지움 (truncate) | ofstream의 기본에 포함 |
std::ios::binary | 이진 모드로 파일 열기 (텍스트 모드의 반대) |
ofstream은 기본적으로std::ios::out | std::ios::trunc모드로 열립니다.ifstream은 기본적으로std::ios::in모드로 열립니다.fstream은 기본적으로std::ios::in | std::ios::out모드로 열립니다. 다만binary,trunc,app등 의도가 있는 모드는 명시적으로 지정하는 것이 안전합니다.
바이너리 모드 실습은 다음 절 ‘바이너리 파일 입출력’에서 별도로 다룹니다.
파일 열기 여부 확인: 파일이 열렸는지 항상 확인해야 합니다. 파일이 없거나 접근 권한이 없는 경우 열기 작업이 실패할 수 있습니다.
if (outputFile.is_open()) { // 또는 if (outputFile)
// 파일 열기 성공
} else {
// 파일 열기 실패
std::cerr << "파일을 열 수 없습니다!" << std::endl;
}파일 읽기/쓰기: 스트림 연산자(<<, >>)나 멤버 함수(read, write, getline 등)를 사용하여 파일에 데이터를 쓰거나 파일에서 데이터를 읽습니다.
// 파일에 쓰기 (출력 스트림)
outputFile << "Hello, File I/O!" << std::endl;
outputFile << 123 << " " << 45.67 << std::endl;
// 파일에서 읽기 (입력 스트림)
std::string line;
int number;
double value;
inputFile >> number >> value; // 공백으로 구분된 데이터 읽기
std::getline(inputFile, line); // 한 줄 전체 읽기파일 닫기 (Close): 파일 작업을 마친 후에는 반드시 파일을 닫아야 합니다. 이는 버퍼에 남아있는 데이터를 파일에 확실히 쓰고, 운영체제가 관리하는 파일 리소스를 해제하여 다른 프로그램이 해당 파일에 접근할 수 있도록 합니다.
outputFile.close();
inputFile.close();스트림 객체가 스코프를 벗어나 소멸될 때 (예: 함수 종료 시 지역 변수인 경우), 자동으로 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;
// 5. 파일 닫기
outFile.close();
} else {
std::cerr << "오류: 'my_output.txt' 파일을 열 수 없습니다!" << std::endl;
}
return 0;
}위 코드를 실행하면, 프로그램이 있는 디렉토리에 my_output.txt라는 파일이 생성되거나 기존 파일의 내용이 지워지고 새로운 내용이 기록됩니다.
간단한 파일 읽기 예제
파일 입출력은 열기, 상태 확인, 작업, 닫기라는 네 단계를 반복합니다. 다음 다이어그램은 ifstream, ofstream, fstream을 고를 때 확인할 책임을 한눈에 정리한 것입니다.
#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;
}
std::cout << "\n파일 'my_output.txt'에서 성공적으로 읽었습니다." << std::endl;
// 5. 파일 닫기
inFile.close();
} else {
std::cerr << "오류: 'my_output.txt' 파일을 열 수 없습니다! (파일이 없거나 권한 문제)" << std::endl;
}
return 0;
}위 코드를 실행하면, my_output.txt 파일의 내용을 한 줄씩 읽어 콘솔에 출력합니다.
파일 입출력 코드는 스트림 종류보다 먼저 “어떤 방향으로 열고, 열기 성공을 확인했는가”를 점검하면 예외 상황을 훨씬 차분하게 다룰 수 있습니다.