에러 처리 모범 사례
개요
Vais는 예외(exceptions) 대신 명시적인 에러 타입을 사용하는 시스템 프로그래밍 언어입니다. 모든 실패 가능한 연산은 Result<T, E> 또는 Option<T> 타입을 반환하여 컴파일 타임에 에러 처리를 강제합니다.
핵심 원칙
- 명시성: 함수 시그니처에서 에러 가능성이 드러남
- 타입 안전성: 컴파일러가 처리되지 않은 에러를 감지
- 제로 비용 추상화: 런타임 오버헤드 없음
Result<T, E> 패턴
기본 구조
Result enum은 성공(Ok)과 실패(Err) 두 가지 상태를 표현합니다:
E Result<T, E> {
Ok(T),
Err(E)
}
함수에서 Result 반환
U std.io
F read_config(path: str) -> Result<str, str> {
file := open(path)?
content := read_to_string(file)?
close(file)
Result.Ok(content)
}
? 연산자로 에러 전파
? 연산자는 Result가 Err일 때 즉시 함수에서 반환합니다:
F parse_and_process(data: str) -> Result<i64, str> {
# parse가 실패하면 즉시 Err 반환
num := parse_i64(data)?
# 검증 실패 시 커스텀 에러
I num < 0 {
R Result.Err("음수는 허용되지 않습니다")
}
Result.Ok(num * 2)
}
! 연산자로 언래핑
! 연산자는 Ok 값을 추출하고, Err일 경우 패닉을 발생시킵니다:
F main() -> i64 {
# 확실히 성공하는 경우에만 사용
config := read_config("app.conf")!
print_str(config)
0
}
주의: !는 프로토타입이나 테스트 코드에서만 사용하고, 프로덕션 코드에서는 명시적 처리를 권장합니다.
M (match)로 Result 처리
F safe_divide(a: i64, b: i64) -> Result<i64, str> {
I b == 0 {
R Result.Err("0으로 나눌 수 없습니다")
}
Result.Ok(a / b)
}
F main() -> i64 {
result := safe_divide(10, 2)
M result {
Result.Ok(value) => {
print_str("결과: ")
print_i64(value)
},
Result.Err(err) => {
print_str("에러: ")
print_str(err)
}
}
0
}
Option<T> 패턴
기본 구조
Option은 값이 있을 수도 없을 수도 있는 상황을 표현합니다:
E Option<T> {
Some(T),
None
}
널 안전성
F find_user(id: i64) -> Option<str> {
I id == 1 {
R Option.Some("Alice")
}
Option.None
}
F main() -> i64 {
user := find_user(1)
M user {
Option.Some(name) => {
print_str("찾은 사용자: ")
print_str(name)
},
Option.None => {
print_str("사용자를 찾을 수 없습니다")
}
}
0
}
?와 ! 연산자 사용
F get_first_char(s: str) -> Option<i64> {
I strlen(s) == 0 {
R Option.None
}
Option.Some(load_byte(s, 0))
}
F process_string(s: str) -> Option<i64> {
# Option에서도 ? 연산자 사용 가능
first := get_first_char(s)?
Option.Some(first + 1)
}
F main() -> i64 {
# ! 연산자로 Some 값 추출 (None이면 패닉)
ch := get_first_char("Hello")!
print_i64(ch)
0
}
커스텀 에러 타입
에러 enum 정의
E FileError {
NotFound(str),
PermissionDenied(str),
IoError(str)
}
F read_file(path: str) -> Result<str, FileError> {
I !file_exists(path) {
R Result.Err(FileError.NotFound(path))
}
I !can_read(path) {
R Result.Err(FileError.PermissionDenied(path))
}
# 실제 읽기 로직...
Result.Ok("file content")
}
에러 변환 패턴
E ParseError {
InvalidFormat(str),
OutOfRange(i64)
}
F parse_positive_number(s: str) -> Result<i64, ParseError> {
# 기본 파싱
M parse_i64(s) {
Result.Ok(num) => {
I num < 0 {
R Result.Err(ParseError.OutOfRange(num))
}
Result.Ok(num)
},
Result.Err(_) => {
Result.Err(ParseError.InvalidFormat(s))
}
}
}
에러 전파 패턴
? 체이닝
U std.io
F load_and_parse_config(path: str) -> Result<i64, str> {
content := read_file(path)?
trimmed := trim(content)?
number := parse_i64(trimmed)?
Result.Ok(number)
}
파이프라인에서의 에러 처리
F process_data(input: str) -> Result<i64, str> {
input
|> validate_input
|> parse_number
|> transform_value
}
F validate_input(s: str) -> Result<str, str> {
I strlen(s) == 0 {
R Result.Err("빈 입력")
}
Result.Ok(s)
}
F parse_number(s: str) -> Result<i64, str> {
parse_i64(s)
}
F transform_value(n: i64) -> Result<i64, str> {
Result.Ok(n * 2)
}
모범 사례
1. 라이브러리 vs 애플리케이션 에러 처리
라이브러리 코드: 항상 Result를 반환하여 호출자가 결정하도록 합니다.
# ✅ 좋은 예: 라이브러리 함수
F lib_parse(data: str) -> Result<i64, str> {
I strlen(data) == 0 {
R Result.Err("빈 데이터")
}
parse_i64(data)
}
애플리케이션 코드: 최상위에서 에러를 처리하거나 의미 있는 에러 메시지를 제공합니다.
# ✅ 좋은 예: 애플리케이션 엔트리포인트
F main() -> i64 {
M run_app() {
Result.Ok(_) => {
print_str("성공적으로 완료되었습니다")
0
},
Result.Err(e) => {
print_str("에러: ")
print_str(e)
1
}
}
}
F run_app() -> Result<i64, str> {
config := load_config()?
data := process_data(config)?
Result.Ok(data)
}
2. 에러 메시지 가이드라인
구체적이고 실행 가능한 정보를 제공하세요:
# ❌ 나쁜 예
F open_file(path: str) -> Result<File, str> {
Result.Err("에러 발생")
}
# ✅ 좋은 예
F open_file(path: str) -> Result<File, str> {
I !file_exists(path) {
R Result.Err("파일을 찾을 수 없습니다: " + path)
}
# ...
}
3. 절대 하지 말아야 할 것들
에러 무시하지 않기:
# ❌ 절대 금지
F bad_example() -> i64 {
_ := might_fail() # 에러 무시!
0
}
# ✅ 올바른 방법
F good_example() -> Result<i64, str> {
might_fail()?
Result.Ok(0)
}
과도한 ! 사용 금지:
# ❌ 나쁜 예: 프로덕션 코드에서 !
F process() -> i64 {
data := read_file("data.txt")!
parse_i64(data)!
}
# ✅ 좋은 예: 명시적 에러 처리
F process() -> Result<i64, str> {
data := read_file("data.txt")?
parse_i64(data)
}
실전 예제
파일 읽기 에러 처리
U std.io
E FileError {
NotFound(str),
ReadError(str)
}
F read_config_file(path: str) -> Result<str, FileError> {
# 파일 존재 확인
I !file_exists(path) {
R Result.Err(FileError.NotFound(path))
}
# 파일 열기
file := M open(path) {
Result.Ok(f) => f,
Result.Err(e) => {
R Result.Err(FileError.ReadError(e))
}
}
# 내용 읽기
content := M read_to_string(file) {
Result.Ok(c) => c,
Result.Err(e) => {
close(file)
R Result.Err(FileError.ReadError(e))
}
}
close(file)
Result.Ok(content)
}
F main() -> i64 {
M read_config_file("config.txt") {
Result.Ok(content) => {
print_str("설정 파일 내용: ")
print_str(content)
0
},
Result.Err(FileError.NotFound(path)) => {
print_str("파일을 찾을 수 없습니다: ")
print_str(path)
1
},
Result.Err(FileError.ReadError(msg)) => {
print_str("읽기 에러: ")
print_str(msg)
2
}
}
}
네트워크 요청 에러 처리
U std.net
E HttpError {
ConnectionFailed(str),
Timeout,
InvalidResponse(i64)
}
F fetch_data(url: str) -> Result<str, HttpError> {
# 연결 시도
conn := M connect(url) {
Result.Ok(c) => c,
Result.Err(e) => {
R Result.Err(HttpError.ConnectionFailed(e))
}
}
# 요청 전송 (타임아웃 5초)
response := M send_request(conn, 5000) {
Result.Ok(r) => r,
Result.Err("timeout") => {
R Result.Err(HttpError.Timeout)
},
Result.Err(e) => {
R Result.Err(HttpError.ConnectionFailed(e))
}
}
# 상태 코드 확인
status := get_status_code(response)
I status != 200 {
R Result.Err(HttpError.InvalidResponse(status))
}
Result.Ok(read_response_body(response))
}
파싱 에러 처리
E JsonError {
SyntaxError(i64, str), # line, message
TypeError(str)
}
F parse_user_json(json: str) -> Result<User, JsonError> {
# JSON 파싱
obj := M parse_json(json) {
Result.Ok(o) => o,
Result.Err(msg) => {
line := find_error_line(json, msg)
R Result.Err(JsonError.SyntaxError(line, msg))
}
}
# 필드 추출
name := M get_string_field(obj, "name") {
Result.Ok(n) => n,
Result.Err(_) => {
R Result.Err(JsonError.TypeError("name 필드가 없거나 문자열이 아닙니다"))
}
}
age := M get_i64_field(obj, "age") {
Result.Ok(a) => a,
Result.Err(_) => {
R Result.Err(JsonError.TypeError("age 필드가 없거나 숫자가 아닙니다"))
}
}
Result.Ok(User { name: name, age: age })
}
S User {
name: str,
age: i64
}
요약
Vais의 에러 처리는 다음 원칙을 따릅니다:
- 명시적 타입:
Result<T, E>와Option<T>로 실패 가능성 표현 ?연산자: 에러 전파를 간결하게!연산자: 프로토타입/테스트 전용, 프로덕션에서는 신중히- 패턴 매칭:
M으로 모든 경우를 처리 - 커스텀 에러 타입: 도메인 특화 에러로 명확성 향상
이러한 패턴을 통해 Vais는 안전하고 유지보수 가능한 시스템 프로그래밍을 지원합니다.