본문 바로가기

파일 읽기

명령줄에서 받은 경로의 텍스트 파일을 읽어 출력하고 존재하지 않거나 읽을 수 없는 파일의 실패 경로를 확인합니다.

이제 file_path 인수로 받은 파일을 읽는 기능을 더합니다. 먼저 테스트에 쓸 예제 파일이 필요합니다. 여러 줄에 걸쳐 같은 단어가 몇 번 되풀이되는 적은 양의 텍스트가 좋습니다. 여기서는 에밀리 디킨슨(Emily Dickinson)의 시를 씁니다. 프로젝트의 맨 위 폴더에 poem.txt 파일을 만들고 시 "I'm Nobody! Who are you?"를 넣습니다.

poem.txt
I'm nobody! Who are you?
Are you nobody, too?
Then there's a pair of us - don't tell!
They'd banish us, you know.

How dreary to be somebody!
How public, like a frog
To tell your name the livelong day
To an admiring bog!

파일 내용 읽기

텍스트를 준비했으면 src/main.rs를 고쳐 파일을 읽는 코드를 더합니다. 새로 더한 것은 use std::fs; 줄과 main 끝에서 파일을 읽어 출력하는 부분이며, 앞 글에서 인수를 읽던 코드는 그대로입니다.

src/main.rs
use std::env;
use std::fs;

fn main() {
    let args: Vec<String> = env::args().collect();

    let query = &args[1];
    let file_path = &args[2];

    println!("Searching for {query}");
    println!("In file {file_path}");

    let contents = fs::read_to_string(file_path)
        .expect("Should have been able to read the file");

    println!("With text:\n{contents}");
}

먼저 use 문으로 표준 라이브러리의 관련 부분을 가져옵니다. 파일을 다루려면 std::fs가 필요합니다. main에서 새로 더한 fs::read_to_string은 file_path를 받아 그 파일을 열고, 파일의 내용을 담은 std::io::Result<String>을 돌려줍니다. 그 뒤에 파일을 읽은 뒤 contents의 값을 출력하는 임시 println! 문을 다시 더해, 지금까지 프로그램이 제대로 동작하는지 확인할 수 있게 합니다. 아래 그림은 fs::read_to_string이 돌려줄 수 있는 두 결과와, 이 글의 코드가 각 결과를 어떻게 다루는지를 보여 줍니다.

파일 경로에서 UTF-8 문자열까지의 Result 경계

poem.txt 전체 UTF-8 읽기→Ok contents 또는 io Error→본문 expect 패닉이라는 요청/결과/호출자 정책이다.

RUST · MINIGREP

파일 읽기의 Result와 호출자의 오류 정책

경로에서 전체 UTF-8 텍스트를 읽는 함수의 결과와, 이 절에서 expect로 그 결과를 처리하는 방법을 구분합니다.

파일 읽기 Result의 두 결과poem.txt를 read_to_string에 전달한다. 성공은 전체 UTF-8 String, 실패는 io::Error이며 이 절은 expect로 실패를 패닉으로 바꾼다.fs::read_to_string입력: poem.txt결과: io::Result<String>Ok(String)파일 전체의 UTF-8 텍스트본문에서는 contents에 저장Err(io::Error)열기·읽기·UTF-8 오류본문의 expect는 패닉 발생성공실패
파일 읽기의 성공과 오류는 두 대안세노드두분기. 파일전체읽기성공은Ok의String이고실패는ioError다. 왼쪽성공레일은Ok로,오른쪽실패레일은Err로직접간다. 둘을순서대로거치는것이아니다. 빈UTF8파일은빈String으로성공하며현재expect는Err를패닉으로바꾼다. 검색일치여부는아직검사하지않는다.fs::read_to_string입력: poem.txtio::Result<String>Ok(String)파일 전체 UTF-8contents에 저장Err(io::Error)열기·읽기·UTF-8이 절의 expect: 패닉성공실패

읽기 호출

fs::read_to_string("poem.txt") → io::Result<String>

성공: Ok(String)

파일 전체의 UTF-8 텍스트를 소유한 String으로 반환하고 본문은 contents에 저장합니다.

실패: Err(io::Error)

열기·읽기 실패나 유효하지 않은 UTF-8이면 Err입니다. 이 절의 expect는 문맥과 오류를 담아 패닉을 발생시킵니다.

현재 코드

fs::read_to_string(file_path)
    .expect("Should have been able \
        to read the file");

실패 처리 방침을 호출하는 쪽에서 정합니다. 11장 3절은 ?로 오류를 돌려줍니다.

파일 경계

임의 바이트에는 fs::read를 사용합니다. 별도의 경로 사전 확인도 나중의 읽기 성공을 보장하지 않습니다. 전체 읽기의 메모리 사용량은 파일 크기에 따라 늘어납니다.

운영체제별 오류 종류와 문구는 달라질 수 있습니다. expect는 고정 문맥뿐 아니라 오류의 Debug 표현도 포함합니다.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
원 도표의 제목·노드·관계·조건·주석
범위원문
관계·조건 설명세 노드와 두 분기다. 파일 전체 읽기의 성공은 Ok의 String이고 실패는 io Error다. 왼쪽 성공 레일은 Ok로, 오른쪽 실패 레일은 Err로 직접 간다. 둘을 순서대로 거치는 것이 아니다. 빈 UTF8 파일은 빈 String으로 성공하며 현재 expect는 Err를 패닉으로 바꾼다. 검색 일치 여부는 아직 검사하지 않는다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
파일 요청

poem.txt 전체를 UTF-8 String으로 요청한다.

  • 함수: read_to_string
  • 파일 읽기와 UTF-8 해석의 실패를 Result로 받는다.
Result 반환

성공 contents와 io Error를 나눈다.

  • Ok: 소유 String contents
  • Err: io Error
  • 성공한 contents가 없는 경로는 일반 출력으로 진행하지 않는다.
본문 정책

이 예의 expect가 실패 시 패닉한다.

  • 성공: contents 사용
  • 실패: expect 패닉
  • expect는 오류 복구가 아니라 이 예제의 호출자 정책이다.

파일 경로에서 UTF-8 문자열까지의 Result 경계

poem.txt 전체 UTF-8 읽기→Ok contents 또는 io Error→본문 expect 패닉이라는 요청/결과/호출자 정책이다.

RUST · MINIGREP

파일 읽기의 Result와 호출자의 오류 정책

경로에서 전체 UTF-8 텍스트를 읽는 함수의 결과와, 이 절에서 expect로 그 결과를 처리하는 방법을 구분합니다.

파일 읽기 Result의 두 결과poem.txt를 read_to_string에 전달한다. 성공은 전체 UTF-8 String, 실패는 io::Error이며 이 절은 expect로 실패를 패닉으로 바꾼다.fs::read_to_string입력: poem.txt결과: io::Result<String>Ok(String)파일 전체의 UTF-8 텍스트본문에서는 contents에 저장Err(io::Error)열기·읽기·UTF-8 오류본문의 expect는 패닉 발생성공실패
파일 읽기의 성공과 오류는 두 대안세노드두분기. 파일전체읽기성공은Ok의String이고실패는ioError다. 왼쪽성공레일은Ok로,오른쪽실패레일은Err로직접간다. 둘을순서대로거치는것이아니다. 빈UTF8파일은빈String으로성공하며현재expect는Err를패닉으로바꾼다. 검색일치여부는아직검사하지않는다.fs::read_to_string입력: poem.txtio::Result<String>Ok(String)파일 전체 UTF-8contents에 저장Err(io::Error)열기·읽기·UTF-8이 절의 expect: 패닉성공실패

읽기 호출

fs::read_to_string("poem.txt") → io::Result<String>

성공: Ok(String)

파일 전체의 UTF-8 텍스트를 소유한 String으로 반환하고 본문은 contents에 저장합니다.

실패: Err(io::Error)

열기·읽기 실패나 유효하지 않은 UTF-8이면 Err입니다. 이 절의 expect는 문맥과 오류를 담아 패닉을 발생시킵니다.

현재 코드

fs::read_to_string(file_path)
    .expect("Should have been able \
        to read the file");

실패 처리 방침을 호출하는 쪽에서 정합니다. 11장 3절은 ?로 오류를 돌려줍니다.

파일 경계

임의 바이트에는 fs::read를 사용합니다. 별도의 경로 사전 확인도 나중의 읽기 성공을 보장하지 않습니다. 전체 읽기의 메모리 사용량은 파일 크기에 따라 늘어납니다.

운영체제별 오류 종류와 문구는 달라질 수 있습니다. expect는 고정 문맥뿐 아니라 오류의 Debug 표현도 포함합니다.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.

관계도의 보충 설명

원 도표의 제목·노드·관계·조건·주석
범위원문
관계·조건 설명세 노드와 두 분기다. 파일 전체 읽기의 성공은 Ok의 String이고 실패는 io Error다. 왼쪽 성공 레일은 Ok로, 오른쪽 실패 레일은 Err로 직접 간다. 둘을 순서대로 거치는 것이 아니다. 빈 UTF8 파일은 빈 String으로 성공하며 현재 expect는 Err를 패닉으로 바꾼다. 검색 일치 여부는 아직 검사하지 않는다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
파일 요청

poem.txt 전체를 UTF-8 String으로 요청한다.

  • 함수: read_to_string
  • 파일 읽기와 UTF-8 해석의 실패를 Result로 받는다.
Result 반환

성공 contents와 io Error를 나눈다.

  • Ok: 소유 String contents
  • Err: io Error
  • 성공한 contents가 없는 경로는 일반 출력으로 진행하지 않는다.
본문 정책

이 예의 expect가 실패 시 패닉한다.

  • 성공: contents 사용
  • 실패: expect 패닉
  • expect는 오류 복구가 아니라 이 예제의 호출자 정책이다.

fs::read_to_string("poem.txt")가 성공하면 파일 전체의 UTF-8 텍스트를 소유한 String이 Ok에 담겨 오고, 코드는 이것을 contents에 저장합니다. 파일을 열거나 읽지 못했거나 내용이 올바른 UTF-8이 아니면 Err(io::Error)가 옵니다. 두 결과는 둘 중 하나만 오는 갈래이며, 빈 UTF-8 파일은 실패가 아니라 빈 String으로 성공합니다. 이 글의 expect는 Err가 오면 패닉을 냅니다. 검색어가 들어 있는지는 아직 확인하지 않습니다.

첫 번째 명령줄 인수에는 아무 문자열이나 주고, 두 번째 인수에는 poem.txt 파일을 주어 실행합니다. 검색 기능은 아직 구현하지 않았습니다.

cargo run -- the poem.txt
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/minigrep the poem.txt`
Searching for the
In file poem.txt
With text:
I'm nobody! Who are you?
Are you nobody, too?
Then there's a pair of us - don't tell!
They'd banish us, you know.

How dreary to be somebody!
How public, like a frog
To tell your name the livelong day
To an admiring bog!

코드가 파일을 읽고 그 내용을 출력했습니다.

파일을 읽지 못했을 때

없는 파일의 경로를 주면 fs::read_to_string이 Err를 돌려주고, expect가 넘긴 메시지와 오류의 Debug 표현을 함께 담아 패닉을 냅니다. 아래 명령은 검색어 the와 함께 없는 파일 이름을 줍니다. 출력의 앞 두 줄은 파일을 읽기 전에 실행되는 println!의 결과이므로, 인수를 읽는 데까지는 성공했다는 것을 알 수 있습니다.

cargo run -- the missing.txt
Searching for the
In file missing.txt

thread 'main' (6017887) panicked at src/main.rs:14:10:
Should have been able to read the file: Os { code: 2, kind: NotFound, message: "No such file or directory" }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

thread 'main' 뒤의 괄호 안 숫자는 스레드의 ID로, 실행할 때마다 다릅니다. 위치 14:10은 14번째 줄에서 expect가 시작하는 칸입니다. 오류의 종류와 message의 문구는 운영체제마다 다르며, 예를 들어 Windows에서는 메시지가 그 시스템의 언어로 나옵니다.

expect는 이렇게 실패를 패닉으로 바꾸지만, 실패를 어떻게 다룰지는 본래 함수를 부른 쪽이 정할 일입니다. 모듈성과 에러 처리 향상을 위한 리팩터링에서는 ?로 오류를 돌려주고 main에서 사용자에게 알맞은 메시지를 출력하도록 고칩니다. 파일을 다룰 때 알아 둘 점도 몇 가지 있습니다. read_to_string은 UTF-8 텍스트만 읽으므로, 아무 바이트나 읽어야 한다면 Vec<u8>을 돌려주는 fs::read를 씁니다. 파일이 있는지 미리 확인해도 그 사이에 파일이 지워질 수 있으므로, 나중의 읽기가 성공한다고 보장하지 않습니다. 그래서 읽은 결과의 Result를 직접 다루는 편이 낫습니다. 또 파일 전체를 한 번에 읽으므로, 파일이 클수록 메모리를 그만큼 씁니다.

지금 코드의 문제점

지금 코드에는 몇 가지 결함이 있습니다. 지금은 main 함수가 여러 일을 맡고 있습니다. 대개 함수 하나가 한 가지 생각만 맡을수록 함수가 더 분명하고 관리하기 쉽습니다. 또 오류를 제대로 다루지 않고 있습니다. 프로그램이 아직 작으므로 이런 결함이 큰 문제는 아니지만, 프로그램이 커질수록 깔끔하게 고치기가 어려워집니다. 프로그램을 만들기 시작한 초기에 리팩터링하는 것이 좋으며, 코드가 적을 때 리팩터링하기가 훨씬 쉽습니다. 다음 글에서 이 작업을 합니다.

정리

  • fs::read_to_string(path)은 파일 전체를 UTF-8 String으로 읽어 io::Result<String>으로 돌려줍니다.
  • 성공하면 Ok(String)이, 열기·읽기 실패나 올바르지 않은 UTF-8이면 Err(io::Error)가 오며, 빈 파일은 빈 String으로 성공합니다.
  • expect는 Err일 때 준 메시지와 오류의 Debug 표현을 담아 패닉을 내며, 오류의 종류와 문구는 운영체제마다 다릅니다.
  • 아무 바이트나 읽으려면 fs::read를 쓰고, 파일이 있는지 미리 확인하기보다 읽은 결과의 Result를 다룹니다.
  • 지금 main은 여러 일을 맡고 있고 오류를 패닉으로만 다루므로, 다음 글에서 리팩터링합니다.

연습 문제

1. 결과가 무엇일까

(가) 비어 있는 empty.txt, (나) 올바른 UTF-8이 아닌 바이트가 든 image.png, (다) 경로가 폴더인 src를 두 번째 인수로 주면 지금 프로그램은 각각 어떻게 되나요?

해설 보기

(가)는 빈 String으로 성공하므로 With text: 다음에 빈 줄이 나오고 정상 종료합니다. (나)는 내용이 올바른 UTF-8이 아니어서 Err가 오고, expect가 패닉을 냅니다. (다)는 폴더를 파일처럼 읽을 수 없으므로 Err가 오고 패닉이 나며, 이때 오류의 종류와 문구는 운영체제에 따라 다릅니다.

2. 미리 확인하면 충분할까

Path::new(file_path).exists()로 파일이 있는지 먼저 확인하고 있을 때만 read_to_string을 부르면, expect가 패닉을 낼 일이 없어질까요?

해설 보기

없어지지 않습니다. 확인한 뒤 읽기 전에 다른 프로그램이 파일을 지울 수 있고, 파일이 있어도 읽을 권한이 없거나 내용이 UTF-8이 아니면 읽기가 실패합니다. 그래서 미리 확인하기보다 read_to_string이 돌려준 Result를 직접 다루는 편이 낫습니다.

다음 글 모듈성과 에러 처리 향상을 위한 리팩터링에서는 main이 맡은 일을 나누고, 패닉 대신 Result로 오류를 다루도록 코드를 고칩니다.