안동민 개발노트 아이콘

안동민 개발노트

8장 : 파일 입출력

텍스트 파일 쓰기

반대로, 프로그램에서 생성된 데이터를 텍스트 파일에 저장하는 방법을 다룹니다.

텍스트 파일에 데이터를 쓰는 것은 사용자 설정 저장, 로그 기록, 결과 출력 등 많은 애플리케이션에서 필수적인 기능입니다.

이 장에서는 std::ofstream을 사용하여 텍스트 파일에 문자열과 숫자를 쓰는 방법, 파일 열기 모드, 출력 버퍼를 다룹니다.

std::ios::outstd::ios::app의 차이, 버퍼에서 파일로 반영되는 시점, fail()/bad() 점검 위치를 한 번에 정리한 다이어그램입니다.


std::ofstream로 텍스트 파일 열기

텍스트 파일에 데이터를 쓰기 위해서는 std::ofstream 클래스의 객체를 사용합니다.

ofstreamOutput File Stream의 약자입니다.

ofstream 객체를 생성할 때 파일 이름을 인자로 전달하여 바로 열거나, 객체 생성 후 open() 멤버 함수를 호출하여 열 수 있습니다.

텍스트 파일 열기 예제
#include <fstream>  // std::ofstream을 위해
#include <iostream> // std::cout, std::endl을 위해

int main() {
    // 방법 1: 생성과 동시에 파일 열기
    std::ofstream outFile1("output1.txt");
    if (!outFile1.is_open()) { // 또는 if (outFile1.fail())
        std::cerr << "output1.txt를 열 수 없습니다!\n";
    } else {
        // 파일 처리...
        outFile1.close();
    }

    // 방법 2: 객체 생성 후 open() 호출
    std::ofstream outFile2;
    outFile2.open("output2.txt");
    if (!outFile2) { // 스트림 객체 자체를 bool 컨텍스트에서 사용하여 성공 여부 확인 가능
        std::cerr << "output2.txt를 열 수 없습니다!\n";
    } else {
        // 파일 처리...
        outFile2.close();
    }

    return 0;
}

std::ofstream은 기본적으로 std::ios::out 모드로 파일을 엽니다. 이 모드는 다음과 같은 특징을 가집니다.

  • 지정된 이름의 파일이 존재하지 않으면 새로 생성합니다.
  • 지정된 이름의 파일이 이미 존재하면, 그 파일의 모든 내용을 지우고(truncate) 새로운 내용을 씁니다. (주의!)

파일 쓰기 (<< 연산자)

콘솔에 std::cout을 사용하여 데이터를 출력하듯이, 파일 스트림에서도 << (삽입) 연산자를 사용하여 데이터를 파일에 쓸 수 있습니다.

다양한 데이터 타입(문자열, 정수, 실수 등)을 파일에 쓸 수 있으며, std::endl을 사용하여 개행 문자를 삽입하고 출력 버퍼를 비울 수 있습니다.

다양한 데이터 쓰기
#include <iostream>
#include <fstream>
#include <string>

int main() {
    std::ofstream outFile("my_data.txt"); // my_data.txt 파일을 쓰기 모드로 엶

    if (!outFile.is_open()) {
        std::cerr << "오류: my_data.txt 파일을 열 수 없습니다!\n";
        return 1;
    }

    // 문자열 쓰기
    outFile << "Hello, File World!\n"; // '\n'은 개행 문자
    outFile << "C++ 파일 입출력 예제입니다." << std::endl; // std::endl은 '\n' + flush

    // 숫자 쓰기
    int age = 30;
    double temperature = 25.7;
    outFile << "나이: " << age << "세" << std::endl;
    outFile << "오늘의 온도: " << temperature << "도" << std::endl;

    // 여러 값 한 줄에 쓰기
    std::string product = "Laptop";
    int quantity = 5;
    double price = 1200.50;
    outFile << product << ", " << quantity << ", " << price << std::endl;

    std::cout << "데이터가 'my_data.txt' 파일에 성공적으로 기록되었습니다." << std::endl;

    outFile.close(); // 파일 닫기
    return 0;
}

위 코드를 실행하면, my_data.txt 파일이 생성되거나 기존 내용이 지워지고 다음과 같은 내용이 기록됩니다.

my_data.txt
Hello, File World!
C++ 파일 입출력 예제입니다.
나이: 30세
오늘의 온도: 25.7도
Laptop, 5, 1200.5

파일 열기 모드 (Open Modes)의 중요성

텍스트 파일 쓰기는 열기 모드, 상태 플래그, 읽기·쓰기 단위, 닫기 책임으로 확인합니다.

텍스트 파일 쓰기에서는 파일을 여는 모드, 버퍼 반영 시점, 쓰기 후 오류 확인 위치를 함께 잡아야 합니다.

std::ios::out 모드는 기본적으로 파일의 내용을 지우고 새롭게 쓰기 시작합니다.

만약 기존 파일의 내용을 유지하면서 새로운 데이터를 추가하고 싶다면, std::ios::app 모드를 사용해야 합니다.

  • std::ios::out (기본값): 파일을 쓰기 모드로 열고, 기존 내용이 있으면 모두 지웁니다.
  • std::ios::app: 파일을 쓰기 모드로 열고, 파일의 맨 끝에 새로운 내용을 추가(append)합니다. 기존 내용은 보존됩니다.
파일 끝에 데이터 추가
#include <iostream>
#include <fstream>
#include <string>
#include <chrono> // 현재 시간을 위해
#include <iomanip> // std::put_time을 위해

int main() {
    std::ofstream logFile;
    // std::ios::app 모드로 파일 열기. 파일이 없으면 생성, 있으면 끝에 추가
    logFile.open("application_log.txt", std::ios::app);

    if (!logFile.is_open()) {
        std::cerr << "오류: application_log.txt 파일을 열 수 없습니다!\n";
        return 1;
    }

    // 현재 시간을 가져와서 로그에 포함
    auto now = std::chrono::system_clock::now();
    auto in_time_t = std::chrono::system_clock::to_time_t(now);

    // 시간 형식을 지정하여 출력
    // std::put_time은 C++11부터 사용 가능하며, <iomanip> 헤더 필요
    logFile << std::put_time(std::localtime(&in_time_t), "%Y-%m-%d %H:%M:%S") << ": ";

    logFile << "애플리케이션이 실행되었습니다." << std::endl;
    logFile << std::put_time(std::localtime(&in_time_t), "%Y-%m-%d %H:%M:%S") << ": ";
    logFile << "사용자 'Admin'이 로그인했습니다." << std::endl;

    std::cout << "로그가 'application_log.txt'에 추가되었습니다." << std::endl;

    logFile.close();
    return 0;
}

위 코드를 여러 번 실행해 보면, application_log.txt 파일의 내용이 계속해서 늘어나는 것을 확인할 수 있습니다.


출력 버퍼와 flush()

파일 스트림은 효율적인 데이터 전송을 위해 버퍼링(Buffering)을 사용합니다.

데이터를 파일에 바로 쓰지 않고, 일단 메모리의 임시 저장 공간(버퍼)에 모아둡니다.

버퍼가 가득 차거나, std::endl을 사용하거나, flush() 멤버 함수를 호출하거나, 파일을 닫을 때(스트림 객체 소멸 시) 버퍼의 내용이 실제 파일에 기록됩니다.

  • std::endl: 개행 문자를 삽입하고 버퍼를 flush()합니다.
  • \n: 단순히 개행 문자만 삽입하고 버퍼를 flush()하지 않습니다.
  • std::flush: 버퍼의 내용을 즉시 파일에 쓰도록 강제합니다.
flush() 사용 예제
#include <iostream>
#include <fstream>
#include <string>
#include <thread> // std::this_thread::sleep_for
#include <chrono> // std::chrono::seconds

int main() {
    std::ofstream outFile("buffered_output.txt");

    if (!outFile.is_open()) {
        std::cerr << "오류: buffered_output.txt 파일을 열 수 없습니다!\n";
        return 1;
    }

    outFile << "이것은 첫 번째 줄입니다.\n"; // '\n'만 사용, flush 안됨
    std::cout << "버퍼에만 쓰여졌을 수 있습니다. 잠시 대기..." << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(2)); // 2초 대기

    // 버퍼의 내용을 강제로 파일에 기록
    outFile.flush();
    std::cout << "버퍼가 플러시되었습니다. 이제 파일에 기록되었을 것입니다." << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(2)); // 2초 대기

    outFile << "이것은 두 번째 줄입니다." << std::endl; // std::endl 사용, flush 됨
    std::cout << "std::endl로 인해 버퍼가 플러시되었습니다." << std::endl;

    outFile.close(); // 파일 닫기 시 자동으로 flush
    std::cout << "파일이 닫혔습니다." << std::endl;

    return 0;
}

일반적으로 std::endl을 사용하여 개행과 플러시를 함께 처리하는 것이 편리합니다.

그러나 실시간으로 데이터가 파일에 기록되어야 하는 매우 중요한 상황(예: 오류 로그, 중요한 데이터 저장)에서는 명시적으로 flush()를 호출하여 데이터 유실을 방지하는 것이 좋습니다.


파일 쓰기 오류 처리

파일 쓰기 중에도 여러 가지 오류가 발생할 수 있습니다.

예를 들어 디스크 공간 부족, 파일 시스템 오류, 접근 권한 없음 등입니다.

읽기에서와 마찬가지로 fail()이나 bad() 멤버 함수를 사용하여 오류를 확인해야 합니다.

파일 쓰기 오류 처리 예제
#include <iostream>
#include <fstream>

int main() {
    std::ofstream outFile("protected_file.txt"); // 쓰기 권한이 없는 파일 또는 디스크 가득 참 가정

    if (!outFile.is_open()) {
        std::cerr << "파일을 열 수 없습니다. 쓰기 권한을 확인하세요.\n";
        return 1;
    }

    outFile << "데이터를 씁니다.";

    if (outFile.fail()) {
        std::cerr << "파일 쓰기 중 오류가 발생했습니다. (fail())\n";
        outFile.clear(); // 오류 플래그 초기화
    }
    if (outFile.bad()) {
        std::cerr << "파일 쓰기 중 심각한 오류가 발생했습니다. (bad())\n";
        outFile.clear(); // 오류 플래그 초기화
    }

    outFile.close();
    return 0;
}

텍스트 파일 쓰기는 열기 성공만으로 끝나지 않으므로, 버퍼 반영과 상태 확인까지 같은 흐름에서 점검해야 합니다.

쓰기 결과가 예상과 다를 때는 증상을 기준으로 모드, 버퍼, 상태 플래그를 거꾸로 추적하면 원인을 빠르게 좁힐 수 있습니다.

ofstream 쓰기는 모드, 버퍼, 상태 플래그를 같이 봐야 파일 손실을 막을 수 있습니다.