PHpullh

C++ · 심층 가이드

C++ 파일/IO 완전 정리

fstream과 std::filesystem을 쓰는 15개 예제로 텍스트 읽기와 쓰기, CSV 파싱, 경로 조작, 설정 로드까지 실패 처리를 포함해 다룹니다.

주제 15개 · 예제 코드 포함 · 최종 수정 2026-09-03 · 작성 pullh 편집팀

파일 입출력은 C++ 표준 라이브러리 중에서 세대 차가 가장 크게 드러나는 영역입니다. 스트림은 언어 초기부터 있던 설계라 상태 플래그로 실패를 알리고, 경로 처리는 C++17에 들어온 std::filesystem이 훨씬 현대적인 인터페이스를 제공합니다. 다행히 공통점은 있습니다. 스트림 객체는 소멸할 때 스스로 닫히므로 명시적 close()가 필요한 자리는 생각보다 적고, 스코프를 잘 나누는 것이 곧 자원 관리가 됩니다.

텍스트 파일 읽기와 텍스트 파일 쓰기, 텍스트 파일 추가 쓰기는 열기 모드 플래그만 바뀌는 한 묶음입니다. 경로 합치기와 존재 여부 확인, 디렉터리 목록은 문자열 조작 대신 std::filesystem::path를 쓰는 연습이라 함께 보면 좋습니다. CSV 파싱과 설정 로드, 직렬화는 결국 한 줄을 읽어 구조로 바꾸는 같은 작업이고, 표준 입력 읽기까지 포함해 입력 소스만 갈아 끼우도록 짜 두면 테스트가 쉬워집니다.

가장 흔한 사고는 실패한 열기를 눈치채지 못하는 것입니다. 존재하지 않는 경로로 std::ifstream을 만들어도 예외는 없고, 그 상태로 읽으면 아무 일도 없이 빈 결과가 나옵니다. 만든 직후 is_open()이나 스트림 자체를 조건으로 검사하는 습관이 필요합니다. CSV도 조심할 부분이 있습니다. getline으로 줄을 자르고 콤마로 나누는 방식은 따옴표로 감싼 필드 안의 콤마나 줄바꿈에서 바로 깨지므로, 진짜 데이터를 다룬다면 인용 처리를 포함한 파서를 쓰거나 직접 상태 기계를 만들어야 합니다.

01텍스트 파일 읽기

std::ofstream으로 sample-1.txt를 열어 hello를 쓰고, close() 후 화면에 written을 남깁니다. 결과가 이미 확정된 데이터를 파일로 떨어뜨릴 때 쓰는 가장 짧은 형태입니다. 기본 열기 모드가 truncate라서 같은 이름의 파일이 있으면 내용이 먼저 통째로 지워진다는 점을 놓치기 쉽습니다.

C++ code

// 텍스트 파일 읽기
#include <fstream>
#include <iostream>

int main() {
    std::ofstream out("sample-1.txt");
    out << "hello";
    out.close();
    std::cout << "written\n";
}
알아두면 좋은 점

기존 내용을 지키려면 std::ofstream out(path, std::ios::app)처럼 두 번째 인자로 모드를 넘겨야 하고, 이 인자는 생성자에서만 지정할 수 있습니다.

자주 하는 실수

쓰기가 실패해도 스트림은 조용히 실패 상태만 남기므로, 마지막에 out.good()을 확인하지 않으면 디스크가 가득 찬 상황을 그대로 놓칩니다.

02텍스트 파일 쓰기

여기서는 파일을 만들지 않고, std::filesystem::path에 / 연산자를 두 번 적용해 만든 경로 문자열만 출력합니다. 로그 디렉터리 규칙처럼 경로를 조립해 확인할 때 쓰는 코드입니다. path의 스트림 출력은 내부적으로 따옴표를 붙이므로, 화면에는 경로가 "logs/2026/app-2.log"처럼 감싸여 나옵니다.

C++ code

// 텍스트 파일 쓰기
#include <filesystem>
#include <iostream>

int main() {
    std::cout << std::filesystem::path("logs") / "2026" / "app-2.log" << "\n";
}
알아두면 좋은 점

따옴표 없이 경로만 얻고 싶다면 path.string()을 출력하면 되고, 이 변환은 플랫폼의 기본 문자 인코딩을 따릅니다.

자주 하는 실수

경로를 문자열로 직접 이어 붙이면 구분자가 두 번 들어가거나 빠지기 쉬우므로, 조립은 path에 맡기고 문자열은 마지막에 한 번만 꺼내야 합니다.

03텍스트 파일 추가 쓰기

제목은 추가 쓰기지만, 실제 코드는 sample-3.txt를 새로 열어 hello를 쓰고 닫는 덮어쓰기 동작을 합니다. 기존 내용 뒤에 이어 붙이려면 생성자에 std::ios::app을 넘겨야 하고, 그때부터는 쓰기 위치가 매 출력마다 파일 끝으로 강제됩니다. 모드 지정 하나가 파일 보존 여부를 가릅니다.

C++ code

// 텍스트 파일 추가 쓰기
#include <fstream>
#include <iostream>

int main() {
    std::ofstream out("sample-3.txt");
    out << "hello";
    out.close();
    std::cout << "written\n";
}
알아두면 좋은 점

std::ios::app은 매 쓰기 직전에 위치를 파일 끝으로 옮기므로, 중간에 seekp로 위치를 바꿔도 실제 기록은 항상 끝에 붙습니다.

자주 하는 실수

여러 프로세스가 같은 로그 파일에 붙여 써도 한 번에 넘기는 양이 크면 줄이 섞일 수 있어, 기록의 원자성은 보장되지 않습니다.

04JSON 저장

JSON 파일을 다루는 대신 logs, 2026, app-4.log를 이어 붙인 경로를 한 줄 찍는 코드입니다. 저장 위치를 코드로 계산해야 하는 상황의 출발점이 됩니다. <filesystem>은 C++17에 들어왔고, GCC 8 이전 툴체인에서는 -lstdc++fs를 링크해야 빌드가 통과합니다.

C++ code

// JSON 저장
#include <filesystem>
#include <iostream>

int main() {
    std::cout << std::filesystem::path("logs") / "2026" / "app-4.log" << "\n";
}
알아두면 좋은 점

std::filesystem의 함수들은 실패하면 예외를 던지지만, std::error_code를 인자로 받는 형태를 쓰면 예외 없이 결과를 확인할 수 있습니다.

자주 하는 실수

설정 파일을 그대로 덮어쓰다가 중간에 프로세스가 죽으면 원본과 새 내용이 모두 사라지므로, 임시 파일에 쓴 뒤 rename으로 바꿔치기해야 합니다.

05CSV 파싱

CSV라는 제목과 달리 코드가 하는 일은 sample-5.txt에 hello 한 덩어리를 기록하는 것뿐입니다. 실제 파싱에는 std::getline으로 줄을 끊고 std::istringstream으로 쉼표를 나누는 단계가 더 필요합니다. <<는 구분자나 줄바꿈을 자동으로 넣어 주지 않으므로 형식은 전부 직접 만들어야 합니다.

C++ code

// CSV 파싱
#include <fstream>
#include <iostream>

int main() {
    std::ofstream out("sample-5.txt");
    out << "hello";
    out.close();
    std::cout << "written\n";
}
알아두면 좋은 점

std::getline은 세 번째 인자로 구분 문자를 받아, 쉼표를 넘기면 줄 대신 필드 단위로 끊어 읽을 수 있습니다.

자주 하는 실수

따옴표 안에 쉼표가 들어 있는 CSV를 단순 분리로 처리하면 필드 수가 어긋나고, 그 줄만 조용히 잘못 해석되어 한참 뒤에 발견됩니다.

06환경 변수 읽기

환경 변수 대신 path 객체를 조합한 결과를 표준 출력으로 보냅니다. 플랫폼마다 다른 구분자를 직접 쓰지 않고 경로를 만들고 싶을 때 이 방식을 씁니다. / 연산자는 필요한 자리에만 구분자를 채워 넣으며, Windows에서는 선호 구분자인 역슬래시가 사용됩니다.

C++ code

// 환경 변수 읽기
#include <filesystem>
#include <iostream>

int main() {
    std::cout << std::filesystem::path("logs") / "2026" / "app-6.log" << "\n";
}
알아두면 좋은 점

path는 filename()과 extension(), parent_path()로 구성 요소를 분해할 수 있어 문자열 탐색 코드를 대신할 수 있습니다.

자주 하는 실수

윈도우 경로 구분자는 역슬래시라 문자열 리터럴에 그대로 적으면 이스케이프로 해석되므로, 조각을 나눠 이어 붙이는 편이 실수를 줄입니다.

07명령행 인자

sample-7.txt를 열고 문자열을 흘려보낸 뒤 명시적으로 닫는 흐름이며, 명령행 인자는 등장하지 않습니다. argc와 argv를 받으려면 main의 시그니처부터 바꿔야 합니다. 참고로 ofstream은 소멸자에서 알아서 닫히므로 close()는 닫는 시점을 앞당기려는 의도일 때만 의미가 있습니다.

C++ code

// 명령행 인자
#include <fstream>
#include <iostream>

int main() {
    std::ofstream out("sample-7.txt");
    out << "hello";
    out.close();
    std::cout << "written\n";
}
알아두면 좋은 점

out.close()를 명시적으로 부르면 그 시점에 실패 여부를 확인할 수 있고, 소멸자에 맡기면 오류를 알아챌 방법이 사라집니다.

자주 하는 실수

이미 닫힌 스트림에 다시 쓰면 실패 비트만 설정되고 아무 내용도 남지 않으므로, 닫는 위치를 잘못 잡으면 데이터가 통째로 사라집니다.

08경로 합치기

path("logs") / "2026" / "app-8.log"처럼 경로 조각을 연달아 결합하는 방법을 그대로 보여 줍니다. 설정값에서 받은 디렉터리와 파일명을 안전하게 붙일 때 적합합니다. 오른쪽 조각이 절대 경로라면 왼쪽에 쌓아 둔 부분이 유지되지 않고 통째로 대체되므로, 사용자 입력을 붙일 때 주의해야 합니다.

C++ code

// 경로 합치기
#include <filesystem>
#include <iostream>

int main() {
    std::cout << std::filesystem::path("logs") / "2026" / "app-8.log" << "\n";
}
알아두면 좋은 점

경로가 허용된 범위 안에 있는지 확인하려면 weakly_canonical로 정규화한 뒤 기준 디렉터리로 시작하는지를 비교해야 합니다.

자주 하는 실수

사용자 입력에 상위 디렉터리를 뜻하는 조각이 섞여 있으면 기준 폴더 밖으로 빠져나가므로, 정규화 없이 그대로 붙이면 엉뚱한 파일에 접근하게 됩니다.

09디렉터리 목록

디렉터리를 훑는 코드는 없고, sample-9.txt에 짧은 문자열을 기록한 다음 상태 메시지를 출력합니다. 목록을 얻으려면 std::filesystem::directory_iterator가 필요합니다. 스트림 출력은 곧바로 디스크에 닿지 않고 버퍼에 머무르기 때문에, flush 전에 프로세스가 죽으면 그 내용은 사라집니다.

C++ code

// 디렉터리 목록
#include <fstream>
#include <iostream>

int main() {
    std::ofstream out("sample-9.txt");
    out << "hello";
    out.close();
    std::cout << "written\n";
}
알아두면 좋은 점

recursive_directory_iterator는 하위 폴더까지 훑어 주지만, 심볼릭 링크가 순환하면 무한히 돌 수 있어 링크 추적 옵션을 확인해야 합니다.

자주 하는 실수

출력이 끝났다고 믿고 std::abort로 프로세스를 끝내면 버퍼에 남은 내용이 비워지지 않아 파일이 잘린 채 남습니다.

10임시 파일

임시 파일을 만드는 대신, 고정된 세 조각으로 app-10.log 경로를 계산해 보여 줍니다. 시스템 임시 디렉터리를 기준으로 삼고 싶다면 std::filesystem::temp_directory_path()를 앞에 두면 됩니다. path는 문자열이 아닌 독립된 타입이라, C 함수에 넘길 때는 .string()이나 .c_str()로 바꿔야 합니다.

C++ code

// 임시 파일
#include <filesystem>
#include <iostream>

int main() {
    std::cout << std::filesystem::path("logs") / "2026" / "app-10.log" << "\n";
}
알아두면 좋은 점

temp_directory_path()는 환경 변수 설정을 참고하므로, 결과 디렉터리가 실제로 존재하고 쓰기 가능한지는 따로 확인해야 합니다.

자주 하는 실수

임시 파일 이름을 고정해 두면 같은 프로그램이 동시에 두 개 돌 때 서로의 내용을 덮어쓰므로, 실행마다 구분되는 이름이 필요합니다.

11존재 여부 확인

존재 여부를 묻는 제목이지만 코드는 sample-11.txt를 만들어 hello를 쓰고 끝냅니다. 검사에는 std::filesystem::exists가 따로 필요합니다. 그리고 스트림은 열기에 실패해도 예외를 던지지 않고 조용히 실패 상태가 되므로, is_open() 확인 없이 <<를 이어 가면 아무 일도 일어나지 않습니다.

C++ code

// 존재 여부 확인
#include <fstream>
#include <iostream>

int main() {
    std::ofstream out("sample-11.txt");
    out << "hello";
    out.close();
    std::cout << "written\n";
}
알아두면 좋은 점

exists로 확인한 뒤 여는 방식은 그 사이에 파일이 바뀔 수 있으므로, 그냥 열어 보고 is_open()으로 판단하는 편이 경합이 없습니다.

자주 하는 실수

존재 여부만 보고 쓰기가 가능하다고 단정하면 권한이 없는 파일에서 열기가 실패하는데, 그 실패는 예외 없이 조용히 지나갑니다.

12표준 입력 읽기

표준 입력을 읽는 부분은 없으며, 조립한 로그 경로 한 줄만 출력합니다. 입력을 받으려면 std::getline(std::cin, line) 같은 호출이 들어가야 합니다. 또한 경로를 만든다고 해서 디렉터리가 생기지는 않으므로, 실제로 쓰기 전에 create_directories로 상위 폴더를 준비해야 합니다.

C++ code

// 표준 입력 읽기
#include <filesystem>
#include <iostream>

int main() {
    std::cout << std::filesystem::path("logs") / "2026" / "app-12.log" << "\n";
}
알아두면 좋은 점

std::getline(std::cin, line)은 스트림 자체를 조건으로 쓸 수 있어, 반복문 조건에 그대로 넣으면 입력 끝까지 자연스럽게 돕니다.

자주 하는 실수

숫자를 읽은 뒤 곧바로 getline을 부르면 남아 있던 줄바꿈을 읽어 빈 줄이 돌아오므로, 중간에 입력 버퍼를 비워야 합니다.

13설정 로드

설정을 불러오는 로직 대신 sample-13.txt에 hello를 기록하는 쓰기 경로만 담겨 있습니다. 설정 파일을 저장하고 다시 읽는 왕복 구조의 절반에 해당합니다. 이 스트림은 텍스트 모드이므로 Windows에서는 \n이 CRLF로 변환되며, 바이트를 그대로 보존하려면 std::ios::binary를 지정해야 합니다.

C++ code

// 설정 로드
#include <fstream>
#include <iostream>

int main() {
    std::ofstream out("sample-13.txt");
    out << "hello";
    out.close();
    std::cout << "written\n";
}
알아두면 좋은 점

바이너리 모드로 열면 개행 변환이 없어 기록한 바이트 수가 파일 크기와 정확히 일치하므로, 체크섬을 다룰 때는 이 모드가 필요합니다.

자주 하는 실수

텍스트 모드로 쓴 파일의 바이트 수를 문자 수와 같다고 가정하면 윈도우에서는 줄 수만큼 어긋나 위치 계산이 전부 밀립니다.

14직렬화

직렬화 포맷을 다루지는 않고, 경로 세 조각을 이어 붙인 값을 출력하는 것으로 끝납니다. 데이터를 어디에 쓸지 정하는 단계로 이해하면 자연스럽습니다. /가 구분자를 넣어 주는 반면 +=는 구분자 없이 문자열을 그대로 잇기 때문에, 확장자를 덧붙일 때와 하위 경로를 만들 때 연산자를 구분해 써야 합니다.

C++ code

// 직렬화
#include <filesystem>
#include <iostream>

int main() {
    std::cout << std::filesystem::path("logs") / "2026" / "app-14.log" << "\n";
}
알아두면 좋은 점

구조체를 통째로 write로 내보내면 패딩과 바이트 순서가 그대로 저장되어, 다른 컴파일러나 아키텍처에서 읽지 못할 수 있습니다.

자주 하는 실수

확장자를 덧붙일 때 경로 결합 연산자를 쓰면 하위 폴더가 만들어지므로, 이럴 때는 replace_extension이나 문자열 덧붙이기를 써야 합니다.

15로그 파일 회전 흉내

회전 로직은 없고 sample-15.txt를 새로 열어 한 번 쓰고 닫는 동작만 있습니다. 실제 회전은 std::filesystem::rename으로 기존 파일을 밀어낸 뒤 다시 여는 순서로 만듭니다. ofstream은 복사할 수 없고 이동만 가능하므로, 열려 있는 스트림을 다른 함수로 넘길 때는 std::move를 써야 합니다.

C++ code

// 로그 파일 회전 흉내
#include <fstream>
#include <iostream>

int main() {
    std::ofstream out("sample-15.txt");
    out << "hello";
    out.close();
    std::cout << "written\n";
}
알아두면 좋은 점

std::filesystem::rename은 같은 파일 시스템 안에서만 원자적이며, 다른 장치로 옮길 때는 복사한 뒤 삭제하는 방식으로 대체해야 합니다.

자주 하는 실수

회전할 때 스트림을 닫지 않은 채 이름을 바꾸면 열린 핸들이 옛 파일을 계속 가리켜, 새 로그가 엉뚱한 곳에 쌓입니다.

정리하며

  • ifstream은 열기 실패를 예외로 알리지 않으므로 생성 직후 is_open으로 확인합니다
  • 경로는 문자열 결합 대신 std::filesystem::path와 / 연산자로 조립합니다
  • getline과 split만으로 만든 CSV 파서는 인용 필드 안의 콤마와 줄바꿈에서 깨집니다
  • 스트림은 소멸 시 자동으로 닫히므로 스코프를 나누는 것만으로 자원 관리가 끝납니다

더 깊이 들어가고 싶다면 C++ 학습 라이브러리에서 다른 주제 가이드를 이어서 보거나, 언어 비교에서 같은 개념이 다른 언어에서 어떻게 표현되는지 확인해 보세요.