Rust 파일 입출력
파일을 읽고 쓰는 일은 지금까지 배운 Result와 ? 연산자가 이론이 아니라 실전이 되는 첫 무대예요. 파일 작업은 거의 다 실패할 수 있어요(파일이 없을 수도 있고, 권한이 없을 수도 있어요). 그래서 표준 라이브러리는 그 상황을 직접 처리하도록 해요.
파일 전체 읽기
섹션 제목: “파일 전체 읽기”파일 내용을 String으로 읽어 오는 가장 간단한 방법이에요.
use std::fs;
fn main() { let contents = fs::read_to_string("hello.txt");
match contents { Ok(text) => println!("{text}"), Err(e) => println!("파일을 읽을 수 없어요: {e}"), }}fs::read_to_string은 Result<String, std::io::Error>를 돌려줘요. 성공하면 텍스트가 담긴 Ok, 실패하면 이유(파일이 없음, 권한 없음 등)가 담긴 Err예요.
?로 에러 처리 간단하게 하기
섹션 제목: “?로 에러 처리 간단하게 하기”파일 작업마다 match를 쓰다 보면 금방 반복이 많아져요. 지금 있는 함수가 Result를 돌려주는 함수라면, (Result 페이지에서 본) ? 연산자로 훨씬 깔끔하게 쓸 수 있어요.
use std::fs;use std::io;
fn read_file(path: &str) -> Result<String, io::Error> { let contents = fs::read_to_string(path)?; // 실패하면 바로 반환해요 Ok(contents)}
fn main() { match read_file("hello.txt") { Ok(text) => println!("{text}"), Err(e) => println!("에러: {e}"), }}파일에 쓰기
섹션 제목: “파일에 쓰기”use std::fs;
fn main() { let result = fs::write("output.txt", "안녕, 파일!");
match result { Ok(()) => println!("잘 저장했어요"), Err(e) => println!("쓰기에 실패했어요: {e}"), }}fs::write는 파일이 없으면 새로 만들고, 이미 있으면 내용을 통째로 덮어써요. 파일을 바꾸는 대신 내용을 덧붙이고 싶을 때는 이 점을 꼭 기억하세요.
파일에 이어 쓰기
섹션 제목: “파일에 이어 쓰기”기존 파일을 덮어쓰지 않고 뒤에 덧붙이려면 OpenOptions를 쓰세요.
use std::fs::OpenOptions;use std::io::Write;
fn main() -> std::io::Result<()> { let mut file = OpenOptions::new() .append(true) .create(true) .open("log.txt")?;
writeln!(file, "새 로그 한 줄")?; Ok(())}여기서는 main 자체가 Result를 돌려준다는 점에 주목하세요. 알아 두면 좋은 패턴이에요. main이 Result를 돌려주면, 모든 걸 match로 감싸는 대신 main 안에서 바로 ?를 쓸 수 있어요.
한 줄씩 읽기
섹션 제목: “한 줄씩 읽기”큰 파일은 한 번에 전부 메모리에 올리는 게 늘 좋은 방법은 아니에요. 대신 한 줄씩 읽어 볼게요.
use std::fs::File;use std::io::{BufRead, BufReader};
fn main() -> std::io::Result<()> { let file = File::open("hello.txt")?; let reader = BufReader::new(file);
for line in reader.lines() { println!("{}", line?); }
Ok(())}어떤 에러인지 확인하기
섹션 제목: “어떤 에러인지 확인하기”지금까지는 모든 에러를 똑같이 처리했어요. 그냥 출력만 했죠. 하지만 무엇이 잘못됐는지에 따라 다르게 대응하고 싶을 때가 많아요. 파일이 없는 건 대개 괜찮아요(처음 실행한 것일 수도 있으니, 빈 상태로 시작하면 돼요). 반면 “권한 없음”은 사용자에게 알려야 할 진짜 문제예요.
io::Error는 .kind()로 자기 종류를 알려 줘요. 이 메서드는 match로 나눌 수 있는 ErrorKind (영어) 열거형을 돌려줘요.
use std::fs;use std::io::ErrorKind;
fn main() { // 시스템 임시 폴더 안의, 존재하지 않는 파일 let path = std::env::temp_dir().join("rusttutorial-missing-notes.txt");
match fs::read_to_string(&path) { Ok(text) => println!("{}줄을 불러왔어요", text.lines().count()), Err(e) => match e.kind() { ErrorKind::NotFound => println!("아직 메모가 없어요. 새로 시작할게요"), ErrorKind::PermissionDenied => println!("메모 파일을 읽을 권한이 없어요"), _ => println!("다른 문제가 생겼어요: {e}"), }, }}실행 결과:
아직 메모가 없어요. 새로 시작할게요std::env::temp_dir()는 시스템 임시 폴더의 경로(Linux라면 /tmp 같은 것)를 주고, .join(...)은 거기에 파일 이름을 붙여요. 파일 함수들은 &str과 마찬가지로 이런 경로도 문제없이 받아요.
마지막에는 항상 _ 갈래를 두세요. ErrorKind에는 변형이 아주 많고, 시간이 지나면서 새로운 것도 추가돼요. _ 갈래가 나열하지 않은 나머지를 모두 받아 줘요.
연습 문제
섹션 제목: “연습 문제”두 문제 모두 먼저 임시 폴더에 직접 파일을 만들기 때문에, Playground를 포함해 어디서든 실행할 수 있어요.
1. 저장하고 줄 세기
섹션 제목: “1. 저장하고 줄 세기”fs::write로 할 일 세 개를 한 줄에 하나씩 파일에 쓰세요. 그다음 fs::read_to_string으로 파일을 다시 읽어서, 각 줄 앞에 번호를 붙여 출력하고 마지막에 전체 개수를 출력하세요.
힌트: 항목 사이에 \n을 넣으면 줄이 나뉘어요. text.lines()는 줄을 하나씩 줘요. ?를 쓸 수 있도록 main이 std::io::Result<()>를 돌려주게 하세요.
정답 보기
use std::fs;
fn main() -> std::io::Result<()> { let path = std::env::temp_dir().join("rusttutorial-todo.txt");
fs::write(&path, "소유권 배우기\n모듈 만들기\n파일 읽기\n")?;
let text = fs::read_to_string(&path)?; let mut count = 0; for line in text.lines() { count += 1; println!("{count}. {line}"); } println!("모두 {count}개"); Ok(())}실행 결과:
1. 소유권 배우기2. 모듈 만들기3. 파일 읽기모두 3개fs::write가 파일 전체를 바꾸기 때문에, 몇 번을 실행해도 똑같은 세 줄이 나와요. 쓰기나 읽기가 실패하면 각 ?가 main에서 에러를 돌려줘요.
2. 로그에 이어 쓰기
섹션 제목: “2. 로그에 이어 쓰기”머리글 한 줄로 로그 파일을 새로 시작한 다음, OpenOptions와 writeln!으로 두 줄을 이어 쓰세요. 마지막으로 파일 전체를 다시 읽어서 출력하세요.
힌트: 머리글은 fs::write로 쓰고(그래야 매번 깨끗하게 시작해요), 추가할 줄은 OpenOptions::new().append(true).open(&path)?로 쓰세요. 파일이 이미 줄바꿈으로 끝나니까, println! 대신 print!를 쓰면 마지막에 빈 줄이 더 생기지 않아요.
정답 보기
use std::fs::{self, OpenOptions};use std::io::Write;
fn main() -> std::io::Result<()> { let path = std::env::temp_dir().join("rusttutorial-log.txt");
fs::write(&path, "=== 로그 ===\n")?; // 새로 시작해요
let mut file = OpenOptions::new().append(true).open(&path)?; writeln!(file, "시작")?; writeln!(file, "완료")?;
print!("{}", fs::read_to_string(&path)?); Ok(())}실행 결과:
=== 로그 ===시작완료use std::fs::{self, OpenOptions};는 fs 모듈 자체와 OpenOptions를 함께 가져와요. 처음의 fs::write가 없으면 실행할 때마다 예전 파일에 계속 덧붙여져서 로그가 끝없이 길어져요. 진짜 로그라면 바로 그게 원하는 동작이지만, 출력이 정해진 연습 문제에서는 곤란하죠.
fs::read_to_string(path)는 파일 전체를String으로 읽고,Result를 돌려줘요.fs::write(path, contents)는 파일을 써요. 파일이 이미 있으면 덮어써요.OpenOptions::new().append(true)를 쓰면 파일을 바꾸는 대신 뒤에 덧붙일 수 있어요.main() -> std::io::Result<()>로 쓰면 호출마다match할 필요 없이main안에서 바로?를 쓸 수 있어요.e.kind()는ErrorKind::NotFound처럼 어떤 종류의 에러인지 알려 줘서, 종류별로 다르게 처리할 수 있어요.- 프로젝트 폴더의 파일을 쓰는 예제는 내 컴퓨터에서 실행하는 게 좋아요.
std::env::temp_dir()는 어디서든 쓸 수 있는 폴더를 줘요.
이해도 점검
섹션 제목: “이해도 점검”이해도 점검
1. 이미 있는 경로에 fs::write를 호출하면 어떻게 될까요?
2. 파일을 덮어쓰지 않고 뒤에 덧붙이려면 어떻게 할까요?
3. main 자체가 std::io::Result<()>를 돌려주면 무엇을 할 수 있게 될까요?
4. fs::read_to_string(path)?가 실패하면 ? 연산자는 무엇을 할까요?
5. 큰 파일을 fs::read_to_string 대신 BufReader로 한 줄씩 읽는 이유는 무엇일까요?
점수: 0 / 5
더 알아보기
섹션 제목: “더 알아보기”std::fs— 표준 라이브러리 문서 (영어):create_dir_all,copy,rename,remove_file처럼 파일과 폴더를 다루는 모든 함수가 있어요.- 파일 읽기 — The Rust Programming Language 한국어판: 공식 문서의
minigrep프로젝트에서 파일을 읽어요. 이 페이지 내용과 명령줄 인자를 함께 써 볼 수 있어요. - File I/O — Rust by Example (영어): 파일 열기, 만들기, 한 줄씩 읽기를 직접 실행해 볼 수 있는 예제예요.
OpenOptions— 표준 라이브러리 문서 (영어): 이어 쓰기, 만들기, 비우기(truncate) 등 파일을 여는 모든 방법을 정리했어요.io::ErrorKind— 표준 라이브러리 문서 (영어):match로 나눌 수 있는 에러 종류의 전체 목록이에요.