Result 를 이용한 에러 핸들링



Result<T, E> 는 enum 타입이며, 주로 에러가 발생할 수 있는 함수에서 리턴 타입으로 사용됨. 


enum Result<T, E> {
Ok(T),
Err(E),
}

보다시피 제너릭 타입임.


Result<T, E> 타입에는 Ok, Err 이라는 요소가 있음. Option<T> 때와 비슷하게 생각하면 됨. Ok 인 경우는 함수(또는 메서드)의 동작이 정상적으로 수행된 경우, Err 인 경우는 비정상적인 상황을 만나 함수가 정상적으로 수행되지 못했음을 알리는 것.


Ok 는 안에 정상적인 함수의 리턴값을 담고, Err 은 에러값을 담음.


예제로 보자면,


use std::fs::File;

fn main() {
let f = File::open("hello.txt");
}


위 코드는 hello.txt 라는 파일을 찾아서 열기를 시도함. 그런데 hello.txt 파일이 존재하지 않아도 실행에 아무런 에러가 없음. 왜냐하면 File::open 함수는 Result<T, E> 를 리턴하기 때문. 바로 에러를 내고 프로그램이 죽어버리는 것이 아니라, 파일이 있을 경우 Ok 에 파일을 담아서 리턴하고, 파일이 없으면 Err 에 에러를 담아서 리턴하는 것. 사용자는 이걸 알고 경우에 맞는 처리를 하면 됨.


use std::fs::File;

fn main() {
let f = File::open("hello.txt");

let f = match f {
Ok(file) => file,
Err(error) => {
panic!("There was a problem opening the file: {:?}", error);
},
};
}


위 코드는 File::open 함수가 파일을 정상적으로 찾고 열었으면 변수 f 에 파일을 넣고, panic! 을 던지고 프로그램을 종료함.


실제로 파일이 없으므로 실행 결과는 아래와 같음.




에러 종류에 따른 처리


에러는 다양한 종류가 있을 수 있고, 에러의 종류에 따라 대응하는 코드가 달라질 수 있음. 긴 말이 필요없고, 아래 코드를 보면 됨.


use std::fs::File;
use std::io::ErrorKind;

fn main() {
let f = File::open("hello.txt"); // 파일 오픈 시도

let f = match f {
Ok(file) => file, // 파일이 존재하고 읽는 데 문제가 없을 경우 file 리턴
Err(ref error) if error.kind() == ErrorKind::NotFound => { // 에러가 발생했는데 그 에러가 ErrorFind::NotFound 일 경우
match File::create("hello.txt") { // 새로운 파일을 만들기
Ok(fc) => fc, // 파일이 정상적으로 만들어졌으면 그 새 파일을 리턴
Err(e) => { // 파일 생성 중 에러가 발생했으면
panic!("Tried to create file but there was a problem: {:?}", e); // panic!
}
}
},
Err(e) => { // 파일 열기 중 발생한 에러가 ErrorKind::NotFound 가 아닌 다른 에러라면
panic!("There was a problem opening the file: {:?}", e); // panic!
},
};
}


읽기 시도하는 파일이 없을 경우 File::open 함수가 전달하는 에러는 std::io::ErrorKind 라는 enum 타입의 NotFound 요소임. 위와 같은 방법으로 에러의 종류에 따라 처리를 달리 할 수 있음. 


if error.kind() == ErrorKind::NotFound 코드를 match guard 라 함. 그리고 Err(ref error) 의 ref 는 error 가 match guard 에 사용되면서도 오너십을 잃지 않게 하기 위함임. 이에 대해서는 후에 자세히 다룸.



에러 처리 코드 줄이기


자주 사용되어 반복되는 에러 처리 코드를 줄이는 방법이 있음. unwrap/expect 를 사용하는 것.


unwrap 은 Result 가 Ok 이면 Ok 안의 값을 리턴하고, Err 이면 알아서 panic! 을 호출함.


fn main() {
let f = File::open("hello.txt").unwrap();
}


expect 는 unwrap 과 동일하게 동작하되, panic! 의 메시지를 다르게 지정할 수 있다는 차이만 있음.


let f = File::open("hello.txt").expect("Failed to open hello.txt");



에러 전파(propagating)


코드에서 에러가 발생할 경우 때로는 에러를 직접 처리하지 않고 그 코드를 호출한 코드에게 넘겨버리는게 좋을 때도 있음(자바에서 익셉션을 throw 할 때와 같이). 이럴 땐 다음과 같이 처리함.


fn read_file() -> Result<String, io::Error> {
let f = File::open("hello.txt").unwrap();

let mut f = match f {
Ok(file) => file,
Err(e) => return Err(e),
}

let mut s = String::new();

match f.read_to_string(&mut s) {
Ok(_) => Ok(s),
Err(e) => Err(e);
}
}

읽을 파일이 없으면 더 이상 함수를 수행할 수 없으므로 에러를 그냥 던짐


보다시피, 당연히, 함수의 리턴 타입은 Result 가 와야 함. 위 함수는 에러가 발생하면 함수 호출 코드에게 에러 처리를 넘기고, 정상적으로 실행되면 파일의 내용을 스트링으로 리턴함(Ok(s))



에러 전파 코드 줄이기


에러 전파 코드는 자주 반복될 수 있음. '?' 를 사용해서 코드를 줄일 수 있음.


앞의 read_file 함수를 아래와 같이 줄일 수 있음.


fn read_file() -> Result<String, io::Error> {
let mut f = File::open("hello.txt");
let mut s = String::new(0;
f.read_to_string(&mut s)?;
Ok(s);
}


위의 read_file 함수는 이전의 함수와 거의 똑같이 동작함. 차이점이 있다면, '?' 의 동작 방식임. '?' 는 std 라이브러리의 From 트레잇에 정의된 from 함수를 사용함. 


코드 실행 중 에러가 발생하고 -> '?' 에 의해 from 함수가 호출되면 -> from 함수는 발생한 에러를 현재 함수의 리턴 타입에 맞는 에러로(예제에서는 io::Error로) 변환해줌. 이로 인해 한 함수가 여러가지 이유로 다른 에러가 발생해도 하나의 에러 타입으로 에러를 던질 수 있게 됨. 아무 에러나 이런 처리가 되는 것은 아니고, 에러 타입이 from 함수를 구현하여 이 에러가 어떻게 변환되어야 하는지 정의되어 있어야 함. 이 조건만 충족되면 '?' 는 알아서 에러를 변환해서 던져줌.


그리고 '?' 의 좋은 기능이 더 있음. 


fn read_file() -> Result<String, io:Error> {
let mut s = String::new();

File::open("hello.txt")?.read_to_string(*mut s)?;
Ok(s);
}


이전의 함수를 위와 같이 줄일 수 있음. File::open("hello.txt") 에서 에러가 발생하면 '?' 가 알아서 함수 실행을 중단하고 에러를 던지고, 정상적으로 파일이 열리면 파일을 리턴함. 그리고 그 파일에 바로 read_to_string 함수를 사용하는 것임. 제이쿼리나 여러가지 빌더 패턴에서 보는 체이닝 코드임.


'?' 는 반드시 리턴 타입이 Result 인 함수를 호출할 때만 사용할 수 있음. 왜냐하면 '?' 는 Result 타입을 다룰 때의 match 와 같은 방식으로 동작하기 때문임. match 를 Result 에 사용하되, Err 를 만나면 return Err(e) 를 수행하는 것이 '?' 의 동작 방식임. 때문에 리턴 타입이 Result 여야만 '?' 를 사용 가능. 아니면 컴파일 에러 발생.




'Rust' 카테고리의 다른 글

트레잇(Traits)  (0) 2018.02.28
제너릭 타입  (0) 2018.02.27
에러 핸들링(panic)  (0) 2018.02.26
컬렉션(해시맵)  (0) 2018.02.25
컬렉션(스트링)  (0) 2018.02.24

+ Recent posts