Rust 파일 I/O 기초: std::fs로 읽고 쓰기
Rust로 코드를 짜다 보면 파일 한두 개는 꼭 만지게 되는데요. 설정 파일을 읽거나 로그를 쓰거나, 임시 파일을 만들거나 하는 일이죠. Rust 표준 라이브러리의 std::fs 모듈이 이런 작업을 위한 도구를 한가득 제공합니다.
이번 글에서는 std::fs의 가장 기본적인 사용법을 살펴보겠습니다. 경로 다루기는 Path와 PathBuf에서, 효율적인 처리는 BufReader/BufWriter에서, 비동기 처리는 tokio::fs에서 별도로 다룹니다.
한 줄 함수로 빠르게 시작하기
std::fs에는 “파일 한 번에 읽고 끝”, “파일 한 번에 쓰고 끝” 같은 시나리오를 위한 헬퍼 함수가 준비되어 있는데요. 가장 자주 쓰는 두 함수가 read_to_string과 write입니다.
use std::fs;
fn main() -> std::io::Result<()> {
// 파일 전체를 String으로 읽기
let content = fs::read_to_string("config.toml")?;
println!("{content}");
// 문자열을 파일에 쓰기 (없으면 생성, 있으면 덮어쓰기)
fs::write("output.txt", "Hello, Rust!")?;
Ok(())
}
read_to_string은 이름 그대로 파일을 통째로 읽어 String으로 돌려주고, write는 받은 데이터를 파일에 그대로 씁니다. 둘 다 내부적으로 File::open과 File::create를 호출하지만, 사용자 입장에서는 임시 변수 없이 한 줄로 끝낼 수 있죠.
바이트 슬라이스가 필요하면 read_to_string 대신 read를 쓰면 됩니다.
let bytes: Vec<u8> = fs::read("image.png")?;
이런 한 방 함수는 작은 파일에는 편리하지만, 대용량 파일을 메모리에 통째로 올리는 건 비효율적입니다. 줄 단위로 처리해야 한다면 BufReader를 쓰는 편이 낫고요.
File 핸들로 세밀하게 다루기
fs::read_to_string/fs::write 같은 헬퍼는 내부적으로 File이라는 핸들을 만들어 쓰는데요. 더 세밀한 제어가 필요하면 이 핸들을 직접 다룹니다.
use std::fs::File;
use std::io::{Read, Write};
fn main() -> std::io::Result<()> {
let mut file = File::open("config.toml")?;
let mut buf = String::new();
file.read_to_string(&mut buf)?;
println!("{buf}");
let mut out = File::create("output.txt")?;
out.write_all(b"Hello, Rust!")?;
Ok(())
}
File::open은 읽기 전용으로, File::create는 쓰기 전용으로 파일을 엽니다. 이렇게 만든 핸들은 Read/Write 트레이트를 통해 다양한 메서드를 제공하죠. read_to_string, write_all 외에도 read_exact, write_fmt 같은 메서드를 쓸 수 있습니다.
Read와 Write 트레이트를 import하지 않으면 read_to_string 같은 메서드가 보이지 않으니 주의하세요. 보통 use std::io::prelude::*;로 한 번에 가져옵니다.
OpenOptions로 정밀하게 열기
File::open은 읽기, File::create는 쓰기인데요. “기존 파일에 이어 쓰기”나 “있을 때만 만들기” 같은 시나리오는 OpenOptions 빌더로 표현합니다.
use std::fs::OpenOptions;
use std::io::Write;
fn main() -> std::io::Result<()> {
// 기존 로그 파일에 이어 쓰기
let mut log = OpenOptions::new()
.append(true)
.create(true)
.open("app.log")?;
writeln!(log, "이벤트 발생: {}", chrono::Utc::now())?;
Ok(())
}
자주 쓰는 옵션은 다음과 같습니다.
read(true)— 읽기 권한write(true)— 쓰기 권한 (기존 내용은 그대로)append(true)— 끝에 이어 쓰기truncate(true)— 열면서 내용 비우기 (write와 함께)create(true)— 없으면 생성create_new(true)— 없을 때만 생성, 이미 있으면 에러
create_new는 TOCTOU(check-then-use) 경쟁 조건을 막아주는 원자적 연산이라, “이미 있으면 만들지 말고 실패해라”는 락 파일 패턴에 자주 등장합니다.
let lock = OpenOptions::new()
.write(true)
.create_new(true)
.open("/tmp/app.lock")?;
디렉터리 다루기
파일 자체뿐 아니라 디렉터리를 만들고 지우고 순회하는 함수도 std::fs에 들어 있습니다.
use std::fs;
fn main() -> std::io::Result<()> {
// 디렉터리 만들기 (중간 경로까지 한 번에)
fs::create_dir_all("data/cache/today")?;
// 디렉터리 안 항목 순회
for entry in fs::read_dir("data")? {
let entry = entry?;
println!("{:?}", entry.path());
}
// 파일 또는 디렉터리 삭제
fs::remove_file("output.txt")?;
fs::remove_dir_all("data/cache")?;
Ok(())
}
create_dir은 한 단계 디렉터리만 만들고 부모가 없으면 실패하지만, create_dir_all은 필요한 부모를 모두 만들어줍니다. 마찬가지로 remove_dir은 빈 디렉터리만 지우고, remove_dir_all은 안에 든 내용까지 재귀적으로 지우죠.
read_dir은 디렉터리 항목을 이터레이터로 돌려주는데, 각 항목이 또 Result라 두 번 ?를 써야 한다는 점이 살짝 헷갈릴 수 있습니다.
메타데이터 확인하기
파일 크기, 수정 시각, 종류(파일인지 디렉터리인지) 같은 메타데이터는 metadata 함수로 가져옵니다.
use std::fs;
fn main() -> std::io::Result<()> {
let meta = fs::metadata("config.toml")?;
println!("크기: {} bytes", meta.len());
println!("파일인가: {}", meta.is_file());
println!("디렉터리인가: {}", meta.is_dir());
println!("수정 시각: {:?}", meta.modified()?);
Ok(())
}
심볼릭 링크의 메타데이터를 따라가지 않고 링크 자체의 정보를 보고 싶을 땐 symlink_metadata를 씁니다.
io::Error 다루기
std::fs 함수는 모두 io::Result<T>(즉 Result<T, std::io::Error>)를 돌려주는데요. 이 에러는 ErrorKind로 종류를 구분할 수 있어서, match로 변형별 처리하기에 좋습니다.
use std::fs;
use std::io::ErrorKind;
fn load_config() -> String {
match fs::read_to_string("config.toml") {
Ok(content) => content,
Err(e) if e.kind() == ErrorKind::NotFound => {
// 파일이 없으면 기본값
String::from("default = true")
}
Err(e) => panic!("설정 파일 읽기 실패: {e}"),
}
}
NotFound, PermissionDenied, AlreadyExists, Interrupted 같은 변형이 자주 쓰입니다. 모든 에러를 한 바구니로 묶지 말고, 회복 가능한 에러는 별도로 처리하는 게 안전합니다.
마치며
std::fs는 파일 처리에 필요한 거의 모든 일을 표준 라이브러리만으로 끝낼 수 있게 해줍니다. 한 방 함수(read_to_string/write)로 빠르게, File과 OpenOptions로 세밀하게, metadata로 정보 조회까지 한 모듈에 모여 있죠.
다음 단계로는 Path와 PathBuf에서 경로를 안전하게 다루는 방법을 보시면 좋습니다. 대용량 파일을 효율적으로 다루고 싶으시다면 BufReader/BufWriter를, 비동기 컨텍스트에서 파일을 다뤄야 한다면 tokio::fs를 이어서 보시기 바랍니다.
더 자세한 내용은 std::fs 공식 문서를 참고하세요.
This work is licensed under
CC BY 4.0