소개
유용한 프로그램 중 상당수는 파일 시스템과 상호 작용합니다. 기존 데이터를 읽고 처리한 다음 결과를 저장합니다. 이러한 작업은 실패할 수 있으므로 Rust의 파일 API는 성공이 보장된다고 가정하지 않고 Result 값을 반환합니다.
준비된 서비스 로그를 확인하고, Path로 파일 위치를 표현하고, std::fs로 텍스트를 읽고, 간단한 요약을 만든 다음, 다른 파일에 저장합니다. 또한 ?를 사용해 I/O 오류를 전파합니다.
Path를 통해 텍스트 파일 읽기
이 단계에서는 준비된 로그를 확인하고 Rust String으로 읽습니다.
프로젝트는 /home/labex/project/log-summary에 있습니다. 이 디렉터리에는 입력 파일 data/events.log, 나중에 보고서를 저장할 빈 output 디렉터리, 프로그램 소스인 src/main.rs가 있습니다. 프로젝트로 이동합니다.
cd /home/labex/project/log-summary
프로그램이 읽을 내용을 확인할 수 있도록 nano로 입력 파일을 엽니다.
nano data/events.log
파일에는 짧은 레코드 3개가 있습니다. 파일을 수정하지 말고 Ctrl+X를 눌러 종료합니다. 이제 Rust 소스 파일을 엽니다.
nano src/main.rs
미리 작성된 use 문 2개는 std::fs의 파일 함수와 Path 타입을 짧은 이름으로 사용할 수 있게 합니다. 이는 이 실습의 주요 주제가 아니라 가져오기 설정에 해당합니다. 뒤의 모듈 실습에서 경로와 use 선언을 자세히 설명합니다. 반환 타입 std::io::Result<()>는 main이 빈 성공 값 ()으로 완료되거나 I/O 오류를 반환한다는 뜻입니다. 마지막의 Ok(())는 성공한 결과입니다.
Step 1 주석을 다음 코드로 바꿉니다.
let input_path = Path::new("data/events.log");
let contents = fs::read_to_string(input_path)?;
println!("Input: {}", input_path.display());
println!("Contents:\n{}", contents.trim_end());
Path::new는 아무것도 열지 않고 파일 시스템 경로를 참조하는 뷰를 만듭니다. 상대 경로는 Cargo를 실행하는 프로젝트 디렉터리를 기준으로 해석됩니다. fs::read_to_string은 파일을 열고 UTF-8 텍스트를 소유된 String으로 읽은 뒤 Result를 반환합니다. ?는 성공하면 String을 제공하고, 실패하면 I/O 오류를 main에서 반환합니다. display()는 사람이 읽기 좋은 형식으로 경로를 표시하며, trim_end()는 파일 끝의 줄바꿈 때문에 출력에 빈 줄이 추가되지 않도록 합니다.
Ctrl+O를 눌러 저장하고 Enter를 누른 다음 Ctrl+X로 종료합니다. 확인한 후 프로그램을 실행합니다.
cargo check
cargo run --quiet
Input: data/events.log
Contents:
INFO service started
WARN disk nearing limit
INFO backup complete
표시된 경로와 3개의 레코드가 모두 출력되면 프로그램이 의도한 파일을 읽은 것입니다.
메모리에 로드한 텍스트의 레코드 수 세기
이 단계에서는 이미 메모리에 저장된 String의 논리적 줄 수를 셉니다.
lines() 메서드는 원본 문자열을 변경하지 않고 텍스트 줄에 대한 반복자를 만듭니다. count() 소비자는 반복자가 생성한 항목의 수를 반환합니다. 소스 파일을 엽니다.
nano src/main.rs
Step 2 주석을 다음 코드로 바꿉니다.
let line_count = contents.lines().count();
println!("Line count: {line_count}");
저장하고 종료한 다음 실행합니다.
cargo run --quiet
새로 추가된 마지막 줄은 다음과 같아야 합니다.
Line count: 3
입력 파일이 줄바꿈으로 끝나는지와 관계없이 이 코드가 동작합니다. lines()는 줄바꿈 바이트의 수가 아니라 논리적 레코드를 식별하기 때문입니다.
안정적인 요약 문자열 만들기
이 단계에서는 경고 레코드 수를 세고, 두 개의 개수를 저장할 수 있는 텍스트로 형식화합니다.
문자열 메서드 starts_with는 접두사를 확인합니다. 이 메서드를 이미 배운 반복자 어댑터와 함께 사용하면 WARN으로 시작하는 줄만 셀 수 있습니다. 소스 파일을 엽니다.
nano src/main.rs
Step 3 주석을 다음 코드로 바꿉니다.
let warning_count = contents
.lines()
.filter(|line| line.starts_with("WARN"))
.count();
let summary = format!("lines: {line_count}\nwarnings: {warning_count}\n");
println!("Summary ready:\n{}", summary.trim_end());
필터 조건은 경고 줄만 남기고, count()는 필터링된 반복자를 소비합니다. format!은 내용을 출력하지 않고 새 String을 반환합니다. 명시적인 \n 문자는 두 줄을 만들고 마지막에 줄바꿈을 추가하므로 텍스트 파일에 저장하기에 적합합니다.
저장하고 종료한 다음 확인하고 실행합니다.
cargo check
cargo run --quiet
출력의 마지막 부분은 다음과 같아야 합니다.
Summary ready:
lines: 3
warnings: 1
이제 요약이 메모리의 데이터로 준비되었으며, 파일 시스템에 저장할 수 있습니다.
요약 파일 작성 및 다시 읽기
이 단계에서는 준비된 출력 디렉터리에 요약을 저장하고, 실제로 저장된 파일을 다시 읽어 결과를 확인합니다.
fs::write(path, data)는 파일을 만들거나 기존 내용을 전달된 바이트로 완전히 바꿉니다. 기존 내용 뒤에 추가하지는 않습니다. 읽기와 마찬가지로 쓰기 작업도 실패할 수 있으며 I/O Result를 반환하므로 ?로 오류를 전파합니다. 소스 파일을 엽니다.
nano src/main.rs
Step 4 주석을 다음 코드로 바꿉니다.
let output_path = Path::new("output/summary.txt");
fs::write(output_path, &summary)?;
let saved = fs::read_to_string(output_path)?;
println!("Saved {}:\n{}", output_path.display(), saved.trim_end());
&summary는 write가 요약의 바이트를 읽는 동안 요약을 빌려 사용하므로, String의 소유권은 프로그램에 계속 유지됩니다. 파일을 다시 읽으면 쓰기가 성공했을 것이라고 가정하지 않고 실제로 저장된 결과를 확인할 수 있습니다.
저장하고 종료한 다음 확인하고 실행합니다.
cargo check
cargo run --quiet
출력의 마지막 부분은 다음과 같아야 합니다.
Saved output/summary.txt:
lines: 3
warnings: 1
마지막으로 생성된 파일을 직접 확인합니다.
nano output/summary.txt
동일한 요약 두 줄이 표시되어야 합니다. 변경하지 말고 Ctrl+X를 눌러 종료합니다.
요약
Path로 파일 위치를 표현하고, fs::read_to_string으로 UTF-8 텍스트를 읽었습니다. ?로 I/O 오류를 전파하고, 로드한 레코드를 요약한 다음, fs::write로 결과를 저장했습니다. 마지막으로 저장된 파일을 다시 읽어 내용을 확인했습니다.


