Rust 기초: 출력 매크로와 Display, Debug 트레이트

Rust 기초: 출력 매크로와 Display, Debug 트레이트

Rust를 배우다 보면 화면에 무언가를 출력하기 위해 println! 같은 매크로를 많이 사용하게 됩니다. 그런데 이 매크로들 뒤에는 Display와 Debug라는 트레이트가 숨어 있고, 이 둘의 차이를 이해하면 Rust의 출력 메커니즘을 더 깊이 있게 다룰 수 있습니다.

출력 매크로

Rust에는 표준 출력 및 에러 출력을 위한 다양한 매크로(macro)가 있습니다. 아래는 가장 많이 사용하는 출력 관련 매크로들입니다.

  • println!: 표준 출력(stdout)에 줄바꿈과 함께 문자열을 출력합니다
  • print!: 줄바꿈 없이 출력합니다
  • eprintln!: 표준 에러(stderr)에 줄바꿈과 함께 출력합니다
  • eprint!: 줄바꿈 없이 stderr에 출력합니다
  • format!: 문자열을 반환만 하고, 실제 출력하지 않습니다
fn main() {
    let name = "Rust";

    println!("Hello, {}!", name);      // stdout에 출력
    eprintln!("Oops, something broke"); // stderr에 출력
    let msg = format!("Hello, {}!", name); // 문자열 생성만
    println!("{}", msg);
}

출력 포멧

Rust 출력 포멧은 매우 강력합니다. {} 안에 다양한 포맷 옵션을 줄 수 있어 정렬, 숫자 진수 출력, 부동소수점 자릿수 지정 등 다양한 출력 포맷을 조절할 수 있습니다.

main.rs
fn main() {
    let num = 42;

    println!("기본 출력: {}", num);
    println!("너비 지정: {:5}", num);     // "   42"
    println!("0으로 채우기: {:05}", num); // "00042"
    println!("진수 출력: {:x}", num);     // "2a"
    println!("정렬: {:>6}", num);         // 오른쪽 정렬
    println!("정렬: {:<6}", num);         // 왼쪽 정렬
    println!("정렬: {:^6}", num);         // 가운데 정렬
}

컴파일 오류

기본 자료형이 아닌 struct이나 enum을 출력하려고 하면 다음과 같은 컴파일 오류가 발생하는데요.

main.rs
struct User {
    id: u32,
    name: String,
}

fn main() {
    let user = User {
        id: 1,
        name: String::from("Dale"),
    };

    println!("{}", user);
                // ^^^^ `User` cannot be formatted with the default formatter
}

이러한 문제가 발생하는 이유는 사용자가 정의한 자료형을 어떻게 출력해야 할지 모르기 때문입니다.

Display 트레이트

Display 트레이트(trait)의 fmt() 함수를 구현하면 자료형을 어떻게 출력할지 알려줄 수 있습니다.

main.rs
use std::fmt;

struct User {
    id: u32,
    name: String,
}

impl fmt::Display for User {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "ID: {}, 이름: {}", self.id, self.name)
    }
}

fn main() {
    let user = User {
        id: 1,
        name: String::from("Dale"),
    };

    println!("{}", user);
}
ID: 1, 이름: Dale

참고로 Display 트레이트을 구현하면 Rust는 자동으로 ToString 트레이트도 구현해주기 때문에, .to_string() 메서드도 사용할 수 있습니다.

Debug 트레이트

매번 Display 트레이트(trait)을 구현하는 것이 번거로울 수도 있습니다. 만약에 단순히 디버깅 용도로 출력이 필요하다면 대신 Debug 트레이트을 활용할 수 있습니다.

struct이나 enum 위에 #[derive(Debug)]를 붙여주면 자동으로 파생(derive)가 되서 매우 편리합니다. 그리고 출력 포멧은 {} 대신에 {:?} 또는 {:#?}를 사용해야 합니다.

main.rs
#[derive(Debug)]
struct User {
    id: u32,
    name: String,
}

fn main() {
    let user = User {
        id: 1,
        name: String::from("Dale"),
    };

    println!("{:?}", user);   // 한 줄 출력
    println!("{:#?}", user); // 예쁘게 출력
}

{} 안에 다양한 포맷 옵션을 줄 수 있어 정렬, 숫자 진수 출력, 부동소수점 자릿수 지정 등 다양한 출력 포맷을 조절할 수 있습니다.

Display vs. Debug

언제 Display 트레이트을 구현하고, 언제 Debug 트레이트을 파생하면 좋을까요?

사람이 읽기 좋은 형식으로 데이터를 출력하고 싶다면 Display 트레이트을 직접 구현하는 것이 좋습니다. 반면에 개발자가 디버깅 용도로 데이터를 자세히 들여다보기 위한 출력할 때는 Debug 트레이트을 자동 파생시키는 편이 유리합니다.

구분목적사용 방식자동 구현 가능
Display사용자 친화적 출력{}❌ (직접 구현 필요)
Debug디버깅 용도 출력{:?}, {:#?}✅ (#[derive(Debug)])

할당 없이 정수를 문자열로

format!이나 to_string()은 편리하지만 호출할 때마다 힙(heap)에 String을 새로 할당합니다. 로그를 초당 수만 줄씩 찍거나 숫자를 대량으로 직렬화하는 코드라면 이 할당이 무시하기 어려운 비용이 되는데요.

Rust 1.98부터는 모든 정수형에 format_into() 메서드가 생겨서, 미리 잡아둔 버퍼(buffer)에 십진수 표현을 바로 써넣을 수 있습니다. 버퍼 타입인 NumBuffer는 해당 정수형이 가질 수 있는 어떤 값이라도 담을 만한 크기로 잡히기 때문에, 길이가 모자랄 걱정은 하지 않아도 됩니다.

main.rs
use core::fmt::NumBuffer;

fn main() {
    let mut buf = NumBuffer::new();

    for id in [7u32, 42, 1234] {
        let s = id.format_into(&mut buf); // 힙 할당이 일어나지 않습니다
        println!("user-{s}");
    }
}
결과
user-7
user-42
user-1234

반환값은 버퍼에서 빌려온 &str이라 다음 값을 써넣기 전까지만 유효합니다. 덕분에 위처럼 반복문 안에서 버퍼 하나를 계속 재사용할 수 있죠.

format_into()는 write! 기반 포매팅이 거치는 동적 디스패치(dynamic dispatch)를 상당 부분 건너뛰기 때문에 속도도 빠릅니다. 정수를 문자열로 바꾸려고 itoa 같은 외부 크레이트(crate)를 쓰고 있었다면 이제 표준 라이브러리만으로 대체할 수 있습니다.

한 가지 주의할 점은 NumBuffer가 core::fmt에만 있고 std::fmt로는 재수출되지 않았다는 것입니다. 습관대로 use std::fmt::NumBuffer;라고 쓰면 컴파일 오류가 나니 core::fmt에서 가져와야 합니다.

마치며

Rust의 출력 관련 매크로와 Display, Debug 트레이트은 단순한 텍스트 출력 그 이상입니다. 로깅, 에러 핸들링, CLI 유틸리티 등 다양한 곳에서 활용되며, 특히 Rust의 엄격한 타입 시스템과 잘 어우러져 실수 없는 출력을 가능하게 해줍니다.

앞으로 println!을 사용할 때마다 이 매크로 뒤에 숨어 있는 트레이트과 포맷팅 시스템을 떠올려 보세요. Rust의 출력을 마스터하면, 여러분의 코드도 더 깔끔하고 명확해질 것입니다.

출력 매크로 외에 Rust가 제공하는 assert 매크로도 Debug 트레이트와 밀접하게 연관되어 있으니 함께 살펴보시면 좋습니다.

더 자세한 내용은 std::fmt - Rust 공식 문서와 println! - Rust 공식 문서를 참고하세요.

This work is licensed under CC BY 4.0CCBY

개발자를 위한 뉴스레터

달레가 정리한 AI 개발 트렌드와 직접 만든 콘텐츠를 전해드립니다.

Discord