본문으로 건너뛰기

안동민 개발노트

본문 시작

스트림과 파일 입출력 개요

스트림을 통한 데이터 흐름과 파일의 지속성을 이해하고 열기·읽기·쓰기·상태 확인·닫기의 기본 절차를 익힙니다.

지금까지 우리는 키보드로부터 데이터를 입력받고 (std::cin), 화면에 데이터를 출력하는 (std::cout) 콘솔 입출력을 주로 사용해 왔습니다.

하지만 프로그램의 데이터는 일회성으로 사용되는 경우가 많습니다.

프로그램이 종료되면 입력받은 데이터나 처리된 결과는 모두 사라집니다.

실제 응용 프로그램에서는 데이터를 영구적으로 저장하거나, 이전에 저장된 데이터를 불러와서 사용해야 할 필요가 있습니다.

이때 사용되는 것이 바로 파일 입출력(File I/O)입니다.

이 장에서는 C++에서 파일 입출력을 처리하는 기본 개념인 스트림(Stream)과 파일 입출력의 전체 구조를 정리합니다.

프로그램, 형식화 스트림, 스트림 버퍼, 파일과 외부 장치 사이의 입력·출력 방향을 보여 주는 C++ 스트림 구조

C++ · stream architecture · persistence

스트림 API는 버퍼를 거쳐 외부 자원과 값을 주고받는다

형식화는 스트림이, 실제 전송은 연결된 std::streambuf가 맡습니다. 파일은 표준 구성이고 네트워크는 별도 라이브러리나 플랫폼의 버퍼와 어댑터가 필요합니다.

C++ 입력과 출력 스트림의 계층 구조 입력 행에서는 외부 원천에서 버퍼와 istream을 거쳐 프로그램으로 값이 들어오고, 출력 행에서는 프로그램에서 ostream과 버퍼를 거쳐 파일이나 외부 장치로 값이 나간다. INPUT · 외부 → 프로그램 외부 원천 키보드 · 파일 버퍼·어댑터 filebuf / external std::istream >> · getline 프로그램 값 메모리에서 사용 OUTPUT · 프로그램 → 외부 프로그램 값 형식화할 데이터 std::ostream << · write 버퍼·어댑터 filebuf / external 외부 목적지 화면 · 지속 파일
input

외부에서 프로그램으로

원천 → 버퍼·어댑터 → std::istream → 프로그램 순서로 읽습니다. 연결 상태와 읽기 결과를 각각 확인합니다.

output

프로그램에서 외부로

프로그램 → std::ostream → 버퍼·어댑터 → 파일 순서로 씁니다. 모드와 쓰기, 플러시 상태를 확인합니다.

both

양방향 파일 스트림

std::fstreamstd::iostream 계열 API와 파일 버퍼를 결합합니다. 방향과 모드, 읽기와 쓰기 위치를 함께 관리합니다.

network

네트워크는 별도 어댑터

표준 iostream에는 네트워크 스트림이 없습니다. 라이브러리나 플랫폼의 std::streambuf 구현이 필요합니다.

파일 스트림 클래스의 방향과 버퍼 연결
클래스형식화 API데이터 방향확인 책임
std::ifstreamstd::istream파일 → 프로그램연결 상태와 읽기 결과
std::ofstreamstd::ostream프로그램 → 파일모드와 쓰기·플러시 상태
std::fstreamstd::iostream파일 ↔ 프로그램방향·모드와 읽기/쓰기 위치

파일은 실행이 끝나도 값을 남기는 지속 장치입니다. 클래스를 고른 뒤에는 경로·모드·상태와 작업 결과를 확인합니다. 같은 API를 다른 자원에 재사용하려면 그 자원과 통신하는 버퍼나 어댑터가 있어야 합니다.


데이터 지속성 (Data Persistence)의 필요성

우리가 개발하는 프로그램은 종종 사용자 설정, 게임 진행 상황, 문서 내용, 이미지, 데이터베이스 정보 등 다양한 종류의 데이터를 다룹니다.

이러한 데이터가 프로그램 실행 중에만 존재하고 종료 시 사라진다면, 사용자는 매번 데이터를 다시 입력하거나 생성해야 하는 불편함을 겪게 됩니다.

데이터 지속성(Data Persistence)은 프로그램이 종료되더라도 데이터가 영구적으로 저장되어 다음 프로그램 실행 시에도 접근할 수 있도록 하는 능력입니다.

파일은 데이터를 지속적으로 저장하는 가장 기본적인 방법 중 하나입니다.


스트림 (Stream) 개념 이해하기

C++의 입출력은 스트림(Stream)이라는 추상적인 개념을 기반으로 합니다.

스트림은 데이터가 흐르는 통로(flow)를 의미합니다.

물이 흐르는 강물처럼 데이터도 한 곳에서 다른 곳으로 흘러간다고 생각할 수 있습니다.

  • 출력 스트림 (Output Stream): 데이터가 프로그램에서 외부(화면, 파일 등)로 흘러나가는 통로입니다. std::cout이 대표적인 출력 스트림입니다.
  • 입력 스트림 (Input Stream): 데이터가 외부(키보드, 파일 등)에서 프로그램으로 흘러들어오는 통로입니다. std::cin이 대표적인 입력 스트림입니다.

C++ 표준 라이브러리(iostreamfstream 헤더)는 이러한 스트림을 처리하기 위한 클래스들을 제공합니다.

  • std::istream: 모든 입력 스트림의 기본 클래스입니다. std::cin이 이 클래스의 객체입니다.
  • std::ostream: 모든 출력 스트림의 기본 클래스입니다. std::cout, std::cerr, std::clog가 이 클래스의 객체입니다.
  • std::fstream: 파일 입출력을 위한 스트림 클래스입니다.

스트림 개념 덕분에 프로그래머는 형식화 인터페이스(<<, >>, getline 등)를 파일과 콘솔에 일관되게 사용할 수 있습니다.

이 인터페이스를 네트워크에 적용하려면 네트워크와 통신하는 std::streambuf 구현이나 라이브러리·플랫폼 어댑터가 필요합니다. C++ 표준 라이브러리가 네트워크 스트림 자체를 제공하는 것은 아닙니다.

예를 들어, 화면에 출력하는 것과 파일에 출력하는 것은 사용법이 거의 유사합니다.


파일 입출력의 기본 단계

스트림과 파일 입출력은 열기 모드, 상태 플래그, 읽기·쓰기 단위, close 책임으로 나누어 확인합니다.

읽기·쓰기 방향에 맞는 파일 스트림과 openmode를 고르고 연결 상태와 작업 결과를 구분해 확인하는 C++ 파일 입출력 흐름

C++ · file I/O · decision flow

방향을 먼저 고르고, 연결과 작업 상태를 따로 확인한다

클래스의 기본 방향과 명시한 openmode는 같은 규칙이 아닙니다. 특히 std::fstream에 모드를 넘기면 그 조합을 그대로 사용하므로 app만으로 in | out이 생기지 않습니다.

파일 스트림 선택부터 작업 결과 확인까지의 흐름 읽기, 쓰기, 양방향 중 하나를 선택해 ifstream, ofstream, fstream으로 나뉜다. 명시적 모드로 파일을 열고 is_open으로 연결을 확인한 뒤 성공한 경우 작업 결과를 조건으로 반복한다. 데이터 방향은? read · write · both read write both std::ifstream mode | in std::ofstream mode | out std::fstream mode 그대로 모드를 명시하고 open out | app · in | out | app is_open()? 파일 연결 여부 no 오류 처리 failbit · path · 권한 yes 작업 결과로 반복 while (getline(file, line))
  1. 방향 선택

    읽기는 ifstream, 쓰기는 ofstream, 양방향은 fstream을 고릅니다.

  2. 모드 명시

    fstream 추가 쓰기는 out | app, 양방향 추가는 in | out | app처럼 의도를 모두 씁니다.

  3. 연결 확인

    open()is_open()으로 파일 연관 여부를 봅니다.

  4. 작업 결과로 반복

    추출·getline 결과를 조건으로 사용하고 실패 이유는 상태 비트로 해석합니다.

  5. RAII 정리

    보통 소멸자가 닫습니다. 조기 해제나 닫기 오류 확인이 필요할 때만 close()를 명시합니다.

formal mode

클래스마다 모드 합성 규칙이 다르다

ifstreamin, ofstreamout을 합칩니다. fstreamin | out은 모드를 생략했을 때의 기본값이며, 넘긴 모드에는 자동으로 붙지 않습니다.

state

연결, 정상 상태, EOF를 구분한다

is_open()은 파일 연관만, if (file)은 현재 failbit·badbit 부재를 봅니다. eof()eofbit 보고값이므로 읽기 루프 조건을 대신하지 않습니다.

formatted → line

>> 뒤 남은 줄바꿈을 처리한다

in >> number;
std::getline(in >> std::ws, line);

std::ws는 줄바꿈을 포함한 선행 공백을 제거하므로 그 동작이 의도에 맞을 때 사용합니다.

lifetime

정상 경로는 소멸자에 맡긴다

파일 스트림의 수명을 스코프로 제한하면 소멸 시 버퍼가 닫힙니다. 명시적 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 파일의 내용을 한 줄씩 읽어 콘솔에 출력합니다.

파일 입출력 코드는 스트림 종류보다 먼저 “어떤 방향으로 열고, 열기 성공을 확인했는가”를 점검하면 예외 상황을 훨씬 차분하게 다룰 수 있습니다.