icon
8장 : 파일 입출력

텍스트 파일 쓰기

반대로, 프로그램에서 생성된 데이터를 텍스트 파일에 저장하는 방법에 대해 알아보겠습니다.

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

이 장에서는 std::ofstream을 사용하여 텍스트 파일에 문자열과 숫자를 쓰는 방법, 파일 열기 모드의 중요성, 그리고 출력 버퍼에 대해 살펴보겠습니다.


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

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

ofstream은 "Output 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;
}