[Rust] 입력받고 출력하기

알고리즘 문제를 Rust로 풀 때 가장 먼저 부딪히는 게 입출력이에요. C++의 cin/cout, Python의 input()/print()처럼 간단하진 않지만, 한번 익혀 두면 계속 쓸 수 있어요.

기본 출력: println!

Rust
fn main() {
    println!("Hello World!");
}

가장 간단한 출력이에요. stdout은 줄 단위로 flush되고 println!은 매번 lock을 잡기 때문에, 출력이 많으면 느려요.

빠른 출력: BufWriter

Rust
use std::io::{self, Write, BufWriter};

fn main() {
    let stdout = io::stdout();
    let mut out = BufWriter::new(stdout.lock());
    writeln!(out, "Hello World!").unwrap();
}

BufWriter로 stdout을 감싸면 버퍼에 모아서 한꺼번에 출력해요. 출력이 많은 문제에서 차이가 커요. 버퍼에 남은 내용은 out이 drop될 때(보통 main이 끝날 때) flush돼요.

  • stdout.lock() → stdout lock을 한 번만 잡음
  • BufWriter → 버퍼링으로 시스템 콜 횟수를 줄임
  • writeln! → println! 대신 사용하는 매크로

기본 입력: read_line

Rust
use std::io;

fn main() {
    let mut input = String::new();
    io::stdin().read_line(&mut input).unwrap();
    let n: i32 = input.trim().parse().unwrap();
    println!("{}", n);
}

한 줄만 읽을 때 쓸 수 있어요. 간단하지만 여러 줄 읽을 때는 번거로워요.

빠른 입력: BufReader + read_to_string

Rust
use std::io::{self, BufReader, Read};

fn main() {
    let mut input = String::new();
    let mut reader = BufReader::new(io::stdin().lock());
    reader.read_to_string(&mut input).unwrap();

    let mut iter = input.split_whitespace();
    let n: i32 = iter.next().unwrap().parse().unwrap();
}

read_to_string은 EOF까지 전체 입력을 한꺼번에 읽어요. 그래서 터미널에서 직접 실행할 때는 입력 후 Ctrl+Z 다음 Enter(Windows) 또는 Ctrl+D(Linux/Mac)를 눌러야 해요. 백준 같은 온라인 저지에서는 입력 파일 끝에서 EOF가 들어오니 신경 쓸 필요 없어요.

참고로 stdin은 이미 내부 버퍼를 갖고 있어서, BufReader 없이 io::stdin().read_to_string(&mut input)만 써도 돼요.

split_whitespace()는 공백, 탭, 줄바꿈을 모두 구분자로 처리해서 파싱이 편해요.

두 수 입력받아 더하기 (BOJ 1000)

Rust
use std::io::{self, BufReader, BufWriter, Read, Write};

fn main() {
    let mut input = String::new();
    let mut reader = BufReader::new(io::stdin().lock());
    reader.read_to_string(&mut input).unwrap();

    let mut iter = input.split_whitespace();
    let a: i32 = iter.next().unwrap().parse().unwrap();
    let b: i32 = iter.next().unwrap().parse().unwrap();

    let stdout = io::stdout();
    let mut out = BufWriter::new(stdout.lock());
    writeln!(out, "{}", a + b).unwrap();
}

unwrap()이 뭔데?

writeln!이나 parse()는 Result 타입을 반환해요.

  • 성공 (Ok(값)) → unwrap()이 값을 꺼내줌
  • 실패 (Err(에러)) → unwrap()이 panic으로 프로그램 종료

알고리즘 문제에서는 입력 형식이 정해져 있으니 unwrap()으로 간단히 처리해요. 실제 프로덕션 코드에서는 ? 연산자로 에러를 전파하는 게 더 좋아요.

mut와 &mut는 뭔데?

변수 선언할 때 mut

Rust는 변수가 기본적으로 불변(immutable)이에요. 값을 바꾸려면 mut를 붙여야 해요.

Rust
let input = String::new();       // 불변 → 값 변경 불가
let mut input = String::new();   // 가변 → 값 변경 가능

read_to_string이 input에 데이터를 써넣어야 하니까 mut가 필요해요.

함수에 넘길 때 &mut

&는 참조(reference)예요. 값을 복사하지 않고 빌려주는 거예요.

  • &input → 읽기만 가능한 참조 (불변 참조)
  • &mut input → 읽기 + 쓰기 가능한 참조 (가변 참조)
Rust
reader.read_to_string(&mut input).unwrap();
//                    ^^^^^^^^^^
// input을 수정해야 하니까 &mut로 넘긴다

read_to_string은 읽은 데이터를 input 안에 채워 넣어야 하므로 수정할 수 있는 &mut로 넘겨야 해요.

lock()은 왜 하는 거야?

stdout()과 stdin()은 여러 스레드에서 동시에 접근할 수 있어요. 그래서 기본적으로 호출할 때마다 lock을 잡고 풀어요.

Rust
// println!은 내부적으로 매번 이렇게 동작한다
stdout().lock() → 쓰기 → unlock
stdout().lock() → 쓰기 → unlock  // 반복할수록 느림

.lock()을 직접 호출하면 lock을 한 번만 잡고 계속 쓸 수 있어요.

Rust
let stdout = io::stdout();
let locked = stdout.lock();           // 한 번만 lock
let mut out = BufWriter::new(locked); // 이후 계속 사용

stdin도 마찬가지예요. stdin().lock()으로 lock을 한 번 잡아 두면 입력을 읽는 동안 lock/unlock을 반복하지 않아요.

next()는 뭐야?

split_whitespace()는 iterator(반복자)를 반환해요. iterator는 값을 하나씩 순서대로 꺼낼 수 있는 객체예요.

.next()를 호출할 때마다 다음 값을 하나씩 꺼내요.

Rust
let input = "1 2 3";
let mut iter = input.split_whitespace();

iter.next()  // → Some("1")  첫 번째 값
iter.next()  // → Some("2")  두 번째 값
iter.next()  // → Some("3")  세 번째 값
iter.next()  // → None       더 이상 없음

Some과 None은 Option 타입이에요. 값이 있을 수도, 없을 수도 있다는 뜻이에요. unwrap()으로 Some 안의 값을 꺼내요.

Rust
iter.next()           // Some("1")
iter.next().unwrap()  // "1" ← Some을 벗겨낸 값

parse()는 뭐야?

parse()는 문자열을 원하는 타입으로 변환하는 메서드예요. Python의 int(), C++의 stoi()와 같은 역할이에요.

Rust
let s = "42";
let n: i32 = s.parse().unwrap();  // "42" → 42 (i32)

변환할 타입은 변수의 타입 선언으로 정해요.

Rust
let a: i32 = "42".parse().unwrap();    // 정수 32비트
let b: i64 = "42".parse().unwrap();    // 정수 64비트
let c: f64 = "3.14".parse().unwrap();  // 실수 64비트
let d: usize = "42".parse().unwrap();  // 부호 없는 정수 (배열 인덱스 등)

변환에 실패하면 Err를 반환하고, unwrap()이 panic을 일으켜요.

Rust
let n: i32 = "abc".parse().unwrap();  // panic! 숫자가 아님

전체 흐름 정리

입력 "1 2"를 두 정수로 파싱하는 과정을 한 줄씩 따라가 보면 이래요:

Rust
let mut iter = input.split_whitespace();
// iter: ["1", "2"]를 순서대로 꺼낼 수 있는 iterator

let a: i32 = iter.next().unwrap().parse().unwrap();
//           ^^^^^^^^^^^  → Some("1")
//                        ^^^^^^^^^  → "1"
//                                    ^^^^^^^  → Result<i32, _>
//                                              ^^^^^^^^^  → 1 (i32)

next()로 문자열을 꺼내고 → unwrap()으로 Some을 벗기고 → parse()로 숫자로 변환하고 → unwrap()으로 Result를 벗겨요.

정리

느린 방법빠른 방법
입력stdin().read_line()BufReader + read_to_string
출력println!BufWriter + writeln!

알고리즘 문제를 풀 때는 그냥 항상 BufReader + BufWriter 조합을 써요.