Rust 기초: Option과 Result에서 as_deref() 활용하기

Rust 기초: Option과 Result에서 as_deref() 활용하기

Rust에서 Option<String>을 다루다 보면 꽤 답답한 순간이 찾아옵니다. Option 안에 들어있는 String&str과 비교하고 싶은데 타입이 맞지 않아 컴파일러가 거부하는 상황이죠.

fn main() {
    let name: Option<String> = Some(String::from("Rust"));

    if name == Some("Rust") {
        println!("찾았다!");
    }
}
결과
error[E0308]: mismatched types
 --> src/main.rs:4:13
  |
4 |     if name == Some("Rust") {
  |                     ^^^^^^ expected `String`, found `&str`

Option<String>Option<&str>은 서로 다른 타입이라 직접 비교할 수 없습니다. 이런 상황에서 as_deref()를 알고 있으면 아주 깔끔하게 해결할 수 있는데요. 이 글에서는 as_ref()와 비교하면서 as_deref()가 왜 필요하고 어떻게 동작하는지 살펴보겠습니다.

Option에서 소유와 참조

문제의 근본 원인부터 짚어보겠습니다. Option<String>에서 String은 힙에 할당된 문자열 데이터를 소유하는 타입이고, &str은 문자열 데이터를 빌려서 보는 참조 타입입니다. Rust의 타입 시스템은 이 둘을 엄격하게 구분합니다.

fn main() {
    let owned: Option<String> = Some(String::from("hello"));
    let borrowed: Option<&str> = Some("hello");

    // Option<String>과 Option<&str>은 다른 타입
    // owned == borrowed; // 컴파일 에러!
}

String&str을 직접 비교하는 건 됩니다. StringPartialEq<str>을 구현하고 있으니까요.

fn main() {
    let s = String::from("hello");
    println!("{}", s == "hello"); // true — 직접 비교는 됨
}
결과
true

그런데 Option으로 감싸는 순간 이 비교가 안 됩니다. Option<T>PartialEq 구현이 같은 T끼리만 비교하도록 되어 있기 때문입니다. 그래서 Option<String>에서 Option<&str>로 변환하는 과정이 필요해집니다.

as_ref()로는 부족한 이유

가장 먼저 떠오르는 방법은 as_ref()입니다. Option<T>as_ref()Option<&T>를 반환하죠.

fn main() {
    let name: Option<String> = Some(String::from("Rust"));
    let r: Option<&String> = name.as_ref();
    println!("{:?}", r);
}
결과
Some("Rust")

소유권을 넘기지 않고 참조를 꺼낼 수 있죠. 하지만 한 가지 문제가 있습니다. as_ref()의 결과는 Option<&String>이지 Option<&str>이 아닙니다.

fn main() {
    let name: Option<String> = Some(String::from("Rust"));

    // as_ref()는 Option<&String>을 반환
    // Option<&str>과는 여전히 비교 불가
    // name.as_ref() == Some("Rust"); // 컴파일 에러!
}

그래서 map을 통해 한 단계 더 변환해야 합니다.

fn main() {
    let name: Option<String> = Some(String::from("Rust"));

    // as_ref() + map으로 Option<&str> 만들기
    let r: Option<&str> = name.as_ref().map(|s| s.as_str());
    println!("{}", r == Some("Rust"));
}
결과
true

작동은 하지만 as_ref().map(|s| s.as_str())이라는 조합이 좀 번거롭습니다. 이걸 매번 쓰기에는 귀찮죠.

as_deref()가 하는 일

as_deref()는 바로 이 변환을 한 번에 해결합니다. Option<T>에서 TDeref 트레이트를 구현하고 있으면 Option<&T::Target>을 반환합니다.

fn main() {
    let name: Option<String> = Some(String::from("Rust"));

    // as_deref()로 Option<&str>을 바로 얻기
    let r: Option<&str> = name.as_deref();
    println!("{}", r == Some("Rust"));
}
결과
true

StringDeref<Target = str>을 구현하고 있으니까 as_deref()Option<String>Option<&str>로 바꿔줍니다. as_ref().map(|s| s.as_str())을 쓸 필요 없이 as_deref() 한 방이면 됩니다.

처음에 봤던 비교 문제도 깔끔하게 해결됩니다.

fn main() {
    let name: Option<String> = Some(String::from("Rust"));

    if name.as_deref() == Some("Rust") {
        println!("찾았다!");
    }
}
결과
찾았다!

as_deref()의 동작 원리

그렇다면 as_deref() 안에서는 어떤 일이 벌어지는 걸까요? 비밀은 Deref 트레이트에 있습니다.

Deref 트레이트는 * 연산자로 역참조했을 때 어떤 타입이 나오는지를 정의합니다. String의 경우 Deref<Target = str>을 구현하고 있어서, String을 역참조하면 str이 됩니다.

as_deref()는 이 Deref 구현을 활용합니다. 내부적으로 보면 다음과 비슷한 동작을 합니다.

// as_deref()의 내부 동작을 풀어쓰면
fn as_deref(opt: &Option<String>) -> Option<&str> {
    match opt {
        Some(s) => Some(s),  // &String → &str (역참조 강제)
        None => None,
    }
}

Some(s)에서 s&String인데, 반환 타입이 Option<&str>이므로 역참조 강제가 작동하여 &String&str로 자동 변환됩니다.

정리하면 as_ref()as_deref()의 차이는 이렇습니다.

fn main() {
    let text: Option<String> = Some(String::from("hello"));

    // as_ref(): Option<T> → Option<&T>
    let a: Option<&String> = text.as_ref();

    // as_deref(): Option<T> → Option<&T::Target>
    let b: Option<&str> = text.as_deref();

    println!("as_ref:   {:?}", a);
    println!("as_deref: {:?}", b);
}
결과
as_ref:   Some("hello")
as_deref: Some("hello")

출력은 같아 보이지만 타입이 다릅니다. as_ref()Option<&String>을, as_deref()Option<&str>을 돌려줍니다. Deref를 한 단계 더 따라가느냐 마느냐의 차이입니다.

다양한 타입에서 활용

as_deref()String에만 쓸 수 있는 게 아닙니다. Deref 트레이트를 구현한 모든 타입에서 사용할 수 있습니다.

VecDeref<Target = [T]>를 구현하고 있으니까 Option<Vec<T>>Option<&[T]>로 변환할 수 있습니다.

fn sum(numbers: Option<&[i32]>) -> i32 {
    match numbers {
        Some(nums) => nums.iter().sum(),
        None => 0,
    }
}

fn main() {
    let nums: Option<Vec<i32>> = Some(vec![1, 2, 3, 4, 5]);
    println!("합계: {}", sum(nums.as_deref()));

    let empty: Option<Vec<i32>> = None;
    println!("합계: {}", sum(empty.as_deref()));
}
결과
합계: 15
합계: 0

PathBufDeref<Target = Path>를 구현하고 있어서 같은 패턴이 적용됩니다.

use std::path::{Path, PathBuf};

fn show_extension(path: Option<&Path>) {
    match path.and_then(|p| p.extension()) {
        Some(ext) => println!("확장자: {}", ext.to_string_lossy()),
        None => println!("확장자 없음"),
    }
}

fn main() {
    let path: Option<PathBuf> = Some(PathBuf::from("/tmp/data.csv"));
    show_extension(path.as_deref());
}
결과
확장자: csv

Option<PathBuf>Option<&Path>로 바꿔서 함수에 넘기는 거죠. Deref 트레이트의 표준 라이브러리 구현을 알고 있으면 어떤 타입에 as_deref()를 쓸 수 있는지 바로 떠올릴 수 있습니다.

원래 타입as_deref() 결과Deref 관계
Option<String>Option<&str>String → str
Option<Vec<T>>Option<&[T]>Vec<T> → [T]
Option<PathBuf>Option<&Path>PathBuf → Path
Option<OsString>Option<&OsStr>OsString → OsStr
Option<CString>Option<&CStr>CString → CStr
Option<Box<T>>Option<&T>Box<T> → T

패턴 매칭에서의 활용

as_deref()match 표현식과 함께 쓸 때 특히 빛을 발합니다. Option<String>을 패턴 매칭하려면 &String과 매칭해야 하는데, as_deref()를 쓰면 문자열 리터럴과 바로 매칭할 수 있습니다.

fn describe_lang(lang: Option<String>) {
    match lang.as_deref() {
        Some("Rust") => println!("시스템 프로그래밍 언어"),
        Some("Python") => println!("범용 스크립팅 언어"),
        Some(other) => println!("기타 언어: {}", other),
        None => println!("언어를 선택하지 않음"),
    }
}

fn main() {
    describe_lang(Some(String::from("Rust")));
    describe_lang(Some(String::from("Go")));
    describe_lang(None);
}
결과
시스템 프로그래밍 언어
기타 언어: Go
언어를 선택하지 않음

as_deref() 없이 같은 코드를 작성하면 이렇게 됩니다.

fn describe_lang(lang: Option<String>) {
    match lang.as_ref().map(|s| s.as_str()) {
        Some("Rust") => println!("시스템 프로그래밍 언어"),
        // ...
    }
}

as_deref() 하나로 훨씬 간결해지죠.

as_deref_mut()

as_deref()가 불변 참조를 꺼낸다면 as_deref_mut()는 가변 참조를 꺼냅니다. Option<T>에서 TDerefMut을 구현하고 있으면 Option<&mut T::Target>을 반환합니다.

fn main() {
    let mut text: Option<String> = Some(String::from("hello"));

    // Option<&mut str>을 얻어서 내부 문자열을 직접 수정
    if let Some(s) = text.as_deref_mut() {
        s.make_ascii_uppercase();
    }

    println!("{:?}", text);
}
결과
Some("HELLO")

as_deref_mut()으로 Option<&mut str>을 꺼내서 내부 문자열을 직접 수정했습니다. Option을 벗기지 않고도 안에 있는 값을 변경할 수 있어서 편리합니다.

Vec에서도 마찬가지입니다.

fn main() {
    let mut nums: Option<Vec<i32>> = Some(vec![3, 1, 4, 1, 5]);

    if let Some(slice) = nums.as_deref_mut() {
        slice.sort();
    }

    println!("{:?}", nums);
}
결과
Some([1, 1, 3, 4, 5])

Result에서의 as_deref()

Option뿐 아니라 Result<T, E>에도 as_deref()가 있습니다. Result<T, E>as_deref()Result<&T::Target, &E>를 반환합니다.

use std::io;

fn read_config() -> Result<String, io::Error> {
    Ok(String::from("debug=true"))
}

fn main() {
    let config = read_config();

    // Result<String, io::Error> → Result<&str, &io::Error>
    match config.as_deref() {
        Ok("debug=true") => println!("디버그 모드"),
        Ok(other) => println!("설정: {}", other),
        Err(e) => println!("오류: {}", e),
    }
}
결과
디버그 모드

Result에서도 as_deref()Ok 안의 값을 역참조한 슬라이스로 변환해서 문자열 리터럴과 바로 매칭할 수 있습니다.

as_deref_mut()Result에서 동일하게 사용할 수 있습니다.

마치며

as_deref()는 메서드 하나짜리지만 쓸 곳이 정말 많습니다. String&str로, Vec<T>&[T]로, PathBuf&Path로 변환하는 작업이 메서드 하나로 끝납니다.

핵심은 Deref 트레이트와의 연결입니다. as_ref()T의 참조를 꺼낸다면, as_deref()T를 역참조한 타겟의 참조를 꺼냅니다. 이 차이를 알고 나면 as_ref().map(|s| s.as_str()) 같은 번거로운 패턴을 쓸 일이 없어집니다.

패턴 매칭에서 Option<String>이나 Result<String, E>를 문자열 리터럴과 비교해야 할 때, as_deref()를 떠올려 보세요.

더 자세한 내용은 Option::as_deref - Rust 공식 문서를 참고하세요.

This work is licensed under CC BY 4.0 CC BY

개발자를 위한 뉴스레터

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

Discord