Rust는 선언적 프로그래밍을 적극적으로 사용하기에 method chaining을 많이 사용하는데 이는 가독성을 높이고 컴파일러가 최적화를 수행하는데 적합한 형태이기 때문이다. 이 포스팅에서는 method chaining을 유지하면서도 중첩된 Result 혹은 Option 형식을 피할 수 있는 and_then()에 대해 알아본다.
match 문을 이용한 처리
세개의 함수 각각이 다음과 같이 Result를 반환한다고 해보자.
use std::io::{Error, ErrorKind};
/// 1. 입력된 사용자 JSON 검증, 빈 문자열이 아니면 Ok 반환.
fn validate_json(raw: &str) -> Result<&str, Error> {
if raw.is_empty() {
Err(Error::new(ErrorKind::InvalidData, "empty user input"))
} else {
Ok(raw)
}
}
/// 2. 사용자 id 반환
fn get_user_id(username: &str) -> Result<u32, Error> {
if username == "admin" {
Ok(1)
} else {
Err(Error::new(ErrorKind::NotFound, "user not found"))
}
}
/// 3. 토큰 생성
fn generate_token(user_id: u32) -> Result<String, Error> {
if user_id == 1 {
Ok(format!("token_for_user_{}", user_id))
} else {
Err(Error::new(ErrorKind::InvalidInput, "user id unavailable"))
}
}
각 함수들은 1. 유효한 JSON인지 판별하고, 2. 사용자를 검색해서 id를 반환한 후, 3. 해당 사용자에 대한 토큰을 생성해서 반환한다.
이를 match 문을 이용해서 처리 하면 다음과 같이 될 것이다.
fn main() {
let raw_input = "admin";
// 1. JSON 유효성 판별
match validate_json(raw_input) {
Ok(user) => {
// 2. 유효하면 사용자 id 검색
match get_user_id(user) {
Ok(id) => {
// 3. 사용자가 유효하면 토큰 생성
match generate_token(id) {
Ok(tok) => {
// 토큰 출력
println!("{:?}", tok);
},
// 토큰 생성시 에러 처리
Err(e) => eprint!("Error: {e}")
}
},
// 유효하지 않은 사용자
Err(e) => eprint!("Error: {e}")
}
},
// JSON이 유효하지 않음
Err(e) => eprint!("Error: {e}")
}
}
}
모든 match arm들을 명시해야 하는 match 구문의 제약 때문에 Result의 Ok와 Err에 대한 경우를 모두 적어 주어야 하고 이 때문에 코드가 장황하고 가독성이 떨어진다.
map() 활용
이전의 “Rust 프로그래밍에서 map() 활용” 에서 언급 했듯이 map()은 container뿐만 아니라 Result나 Option의 Ok / Some인 경우 동작을 정의하는데 사용할 수 있다. 따라서 map()을 이용하면 다음과 같이 작성할 수 있다.
fn main() {
let raw_input = "admin";
let tok = validate_json(raw_input)
.map(|user| get_user_id(user)
.map(|id| generate_token(id)));
// Ok(Ok(Ok("token_for_user_1")))
print!("{:?}", tok);
}
보기에는 훨씬 깔끔해 졌지만, 각 함수들이 Result를 반환하기 때문에 tok변수의 type이 Result<Result<Result<String, ...>, ...>, ...> 같은 여러번 중첩된 형식이 되어 여러번 unwrap 해주어야 하는 보기 싫은 형태가 된다.
and_then() 활용
이렇게 method chaining으로 중첩된 Result나 Option이 반환되는 경우에는 and_then()을 사용해보자.
fn main() {
let raw_input = "admin";
let tok = validate_json(raw_input)
.and_then(|user| get_user_id(user))
.and_then(|id| generate_token(id));
// Ok("token_for_user_1")
print!("{:?}", tok);
}
이렇게 method chaining을 하면 중복된 반환형식 들이 모두 제거되고 Result<String, Error> 형식으로 tok 변수가 반환되어 훨씬 가독성 높은 코드를 유지할 수 있다.
결론
map()과 and_then()은 모두 Rust의 선언형 프로그래밍을 가능하게 하는 중요한 요소로 비슷해 보이지만, closure가 무엇을 반환하는가에 따라 그 활용이 달라진다.
- map(): 내부 값의 형태만 바꿀때(실패 가능성이 없는 단순 변환)
- and_then(): 변환 과정에서 또 다른
Option이나Result가 발생할 때(실패 가능성이 있는 연쇄 작업)
동일한 코드를 match나 if let으로 작성할 수도 있지만, 이른바 “match hell”의 조짐이 보이고 그로 인해 코드의 가독성이 떨어진다면 and_then()을 활용해 보자.