본문으로 건너뛰기

안동민 개발노트

본문 시작

텍스트 파일 쓰기

std::ofstream으로 문자열과 숫자를 기록하고 덮어쓰기·추가 모드, 버퍼 반영 시점과 쓰기 오류를 관리합니다.

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

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

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

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

ofstream 쓰기는 모드 선택부터 상태 확인까지 한 흐름이다

문자열을 `<<`로 보내는 것만으로는 충분하지 않습니다. 기존 파일을 지울지, 뒤에 붙일지, 버퍼가 언제 파일에 반영되는지, 쓰기 실패를 어디서 볼지 함께 정해야 합니다.

  1. 모드 결정

    out / app 새로 쓸지, 끝에 붙일지 먼저 고릅니다.

  2. 열기 확인

    is_open() 경로와 권한 문제를 쓰기 전에 잡습니다.

  3. 버퍼에 출력

    out << line 문자열과 줄바꿈을 스트림 버퍼에 보냅니다.

  4. 파일 반영

    flush / close 버퍼 내용을 디스크로 밀어냅니다.

  5. 상태 점검

    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)의 중요성

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

열기 모드는 기존 파일을 지울지, 보존할지 결정한다

텍스트 파일 쓰기의 가장 큰 위험은 코드가 아니라 모드 선택에서 시작됩니다. 같은 `<<` 출력이라도 `out`, `trunc`, `app` 조합에 따라 결과 파일이 달라집니다.

  1. 의도
    결과 파일 새로 만들기

    의도 결과 파일 새로 만들기

  2. 코드
    코드 ofstream out(path)
  3. 기존 파일 결과
    기존 파일 결

    기존 내용이 새 출력으로 교체됩니다.

  4. 주의
    주의 실수

    로그를 지울 수 있습니다.

  5. 의도
    로그 이어 쓰기

    의도 로그 이어 쓰기

  6. 코드
    코드 ofstream out(path, ios

    app)

  7. 기존 파일 결과
    기존 파일 결

    기존 내용 뒤에 새 줄이 붙습니다.

  8. 주의
    주의 중복 기록 정책

    따로 봅니다.

  9. 의도
    바이트 그대로 쓰기

    의도 바이트 그대로 쓰기

  10. 코드
    코드 ios

    binary

  11. 기존 파일 결과
    기존 파일 결

    줄바꿈 변환 없이 바이트 단위로 기록합니다.

  12. 주의
    주의 텍스트 파일

    읽기 방식이 달라집니다.

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

같은 출력도 모드와 버퍼 반영 시점에 따라 결과가 달라진다

`operator<<`는 값을 스트림 버퍼에 넣습니다. 파일에 남는 최종 결과는 열기 모드와 flush 또는 close 시점, 그리고 쓰기 상태 확인에 의해 결정됩니다.

  1. mode

    덮어쓰기인지 이어쓰기인지 먼저 고릅니다.

  2. open

    경로와 권한 때문에 열기 실패가 없는지 봅니다.

  3. format

    줄바꿈, 구분자, precision을 명확히 둡니다.

  4. state

    쓰기 후 fail/bad로 디스크 오류를 확인합니다.

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;
}

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

텍스트 쓰기는 열기, 버퍼, 상태 확인까지 한 묶음이다

파일을 열고 데이터를 보낸 뒤에도 버퍼와 상태 플래그가 남아 있습니다. 중요한 저장 코드는 쓰기 후 확인까지 포함해야 안전합니다.

  1. 의도 선택

    mode 새 파일인지 추가 기록인지 결정합니다.

  2. 열기 확인

    if (out) 경로와 권한 문제를 본문 처리 전에 거릅니다.

  3. 버퍼 기록

    << 출력 연산은 먼저 스트림 버퍼에 쌓입니다.

  4. 반영 시점

    flush / close 중요 로그는 명시적으로 밀어냅니다.

  5. 상태 확인

    fail / bad 쓰기 실패와 장치 오류를 마지막에 점검합니다.

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

쓰기 결과가 이상하면 증상에서 모드, 버퍼, 상태로 거꾸로 추적한다

텍스트 쓰기 문제는 대부분 열기 모드, 버퍼 반영 시점, 상태 플래그 중 하나에서 시작됩니다. 증상을 기준으로 보면 원인을 빠르게 좁힐 수 있습니다.

  1. 증상
    기존 내용이 사라짐

    증상 기존 내용이 사라짐

  2. 먼저 볼 위치
    먼저 볼 위치 out / trunc 기본 쓰기 모드

    파일을 비웠는지 봅니다.

  3. 수정 방향
    수정 방향 로그라면 ios

    app 을 명시합니다.

  4. 증상
    파일에 바로 안 보임

    증상 파일에 바로 안 보임

  5. 먼저 볼 위치
    먼저 볼 위치 flush / close 버퍼

    아직 OS로 전달되지 않았을 수 있습니다.

  6. 수정 방향
    수정 방향 중요 기록

    flush 시점을 명확히 둡니다.

  7. 증상
    쓰기 실패

    증상 쓰기 실패

  8. 먼저 볼 위치
    먼저 볼 위치 fail /

    bad 권한, 디스크 공간, 장치 오류를 확인합니다.

  9. 수정 방향
    수정 방향 쓰기 후 상태

    확인하고 사용자에게 원인을 알립니다.

  10. 증상
    형식이 깨짐

    증상 형식이 깨짐

  11. 먼저 볼 위치
    먼저 볼 위치 newline /

    separator 줄바꿈과 구분자 정책을 봅니다.

  12. 수정 방향
    수정 방향 읽기 코드

    같은 형식 계약을 유지합니다.

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

ofstream 상태 확인은 모드, 버퍼, 오류 플래그를 함께 본다

텍스트 파일 쓰기는 문자열을 내보내는 일처럼 보이지만, 실제 버그는 열기 모드, flush 시점, 실패 상태를 놓칠 때 생깁니다.

  1. open

    path + mode 경로와 모드를 결정합니다.

  2. write

    << 문자열과 줄바꿈을 버퍼에 보냅니다.

  3. flush

    endl / flush 필요한 시점에 파일로 반영합니다.

  4. state

    fail / bad 쓰기 실패와 심각 오류를 확인합니다.

  5. 기존 내용 삭제 후 기록

    truncate 기존 내용 삭제 후 기록 설정 파일처럼 전체 내용을 새로 쓰는 경우에 적합하지만 백업을 고려합니다.

  6. 파일 끝에 추가

    append 파일 끝에 추가 로그처럼 기존 기록을 유지해야 할 때 사용합니다.

  7. 버퍼를 OS로 밀어냄

    flush 버퍼를 OS로 밀어냄 `endl`은 줄바꿈과 flush를 함께 하므로 잦은 사용은 비용이 됩니다.

  8. open 성공만으로 충분하지 않음

    error open 성공만으로 충분하지 않음 쓰기 중 디스크 공간, 권한, 장치 오류가 뒤늦게 드러날 수 있습니다.

std::ofstream out(path, std::ios::app);
if (!out) throw std::runtime_error("open failed");
out << line << '\n';
out.flush();
if (!out) throw std::runtime_error("write failed");