에러 처리

토파즈의 강력한 에러 처리 시스템을 마스터하세요. Result 타입, Option 타입, 패턴 매칭을 활용한 안전한 에러 처리 방식과 예외 처리 최적화 전략을 학습합니다.

에러 처리는 견고한 소프트웨어의 핵심입니다. 토파즈는 타입 안전한 에러 처리를 위해 ResultOption 타입을 제공합니다. 🛡️

Result 우선 철학
토파즈 v4에서는 Result?/try를 기본으로 사용합니다. throw는 외부 API 연동을 위한 호환성 설탕에만 사용하세요.

🎯 Result 타입

기본 Result 사용법

// Result<성공타입, 에러타입>
function 나누기(분자: int, 분모: int) -> Result<float, string> {
    if 분모 == 0 {
        return Err("0으로 나눌 수 없습니다")
    }
    return Ok(분자 / 분모)
}

// Result 처리하기
let 결과 = 나누기(10, 2)
match 결과 {
    case Ok(값) => print("결과: {값}")
    case Err(오류메시지) => print("오류: {오류메시지}")
}

// 다양한 에러 케이스
function 파일읽기(경로: string) -> Result<string, string> {
    if 경로.length() == 0 {
        return Err("파일 경로가 비어있습니다")
    }
    
    if !경로.endsWith(".txt") {
        return Err("텍스트 파일만 지원됩니다")
    }
    
    if !파일존재확인(경로) {
        return Err("파일을 찾을 수 없습니다: {경로}")
    }
    
    // 실제 파일 읽기 로직
    return Ok("파일 내용")
}

Result 체이닝

// map으로 성공값 변환
let 결과 = 나누기(10, 2)
    .map(값 =>* 2)
    .map(값 => "결과: {값}")

match 결과 {
    case Ok(메시지) => print(메시지)     // "결과: 10"
    case Err(오류) => print("오류: {오류}")
}

// andThen으로 연쇄 처리
function 제곱근(값: float) -> Result<float, string> {
    if< 0 {
        return Err("음수의 제곱근은 구할 수 없습니다")
    }
    return Ok(Math.sqrt(값))
}

let 최종결과 = 나누기(100, 4)
    .andThen(값 => 제곱근(값))
    .map(값 => "제곱근: {값}")

print(최종결과)  // Ok("제곱근: 5")

// orElse로 에러 복구
function 기본값사용(오류: string) -> Result<float, string> {
    print("기본값 사용: {오류}")
    return Ok(0.0)
}

let 복구된결과 = 나누기(10, 0)
    .orElse(기본값사용)
    
print(복구된결과)  // Ok(0.0)

복합 에러 타입

// 구조화된 에러 타입
enum 파일에러 {
    존재하지않음(경로: string),
    권한없음(경로: string),
    읽기실패(이유: string),
    형식오류(예상: string, 실제: string)
}

function 설정파일읽기(경로: string) -> Result<{ [string]: any }, 파일에러> {
    if !파일존재확인(경로) {
        return Err(파일에러.존재하지않음(경로: 경로))
    }
    
    if !읽기권한확인(경로) {
        return Err(파일에러.권한없음(경로: 경로))
    }
    
    match 파일내용읽기(경로) {
        case Ok(내용) => {
            match JSON.parse(내용) {
                case Ok(설정) => return Ok(설정)
                case Err(_) => return Err(파일에러.형식오류(
                    예상: "JSON",
                    실제: "잘못된 형식"
                ))
            }
        }
        case Err(이유) => return Err(파일에러.읽기실패(이유: 이유))
    }
}

// 구체적인 에러 처리
match 설정파일읽기("config.json") {
    case Ok(설정) => {
        print("설정 로드 성공")
        print("테마: {설정.theme}")
    }
    case Err(파일에러.존재하지않음(경로: 경로)) => {
        print("설정 파일이 없습니다: {경로}")
        기본설정생성()
    }
    case Err(파일에러.권한없음(경로: 경로)) => {
        print("파일 읽기 권한이 없습니다: {경로}")
    }
    case Err(파일에러.형식오류(예상: 예상, 실제: 실제)) => {
        print("형식 오류 - 예상: {예상}, 실제: {실제}")
    }
    case Err(파일에러.읽기실패(이유: 이유)) => {
        print("파일 읽기 실패: {이유}")
    }
}

🔍 Option 타입

기본 Option 사용법

// Option<T> - 값이 있을 수도, 없을 수도 있음
function 사용자찾기(아이디: int) -> Option<{ 이름: string, 나이: int }> {
    let 사용자들 = [
        { 아이디: 1, 이름: "김토파즈", 나이: 25 },
        { 아이디: 2, 이름: "이개발", 나이: 30 }
    ]
    
    for 사용자 in 사용자들 {
        if 사용자.아이디 == 아이디 {
            return Some({ 이름: 사용자.이름, 나이: 사용자.나이 })
        }
    }
    
    return None
}

// Option 처리하기
match 사용자찾기(1) {
    case Some(사용자) => print("사용자 발견: {사용자.이름}, {사용자.나이}세")
    case None => print("사용자를 찾을 수 없습니다")
}

// 배열에서 요소 찾기
function 첫번째요소<T>(배열: [T]) -> Option<T> {
    if 배열.length() > 0 {
        return Some(배열[0])
    }
    return None
}

let 숫자들 = [1, 2, 3, 4, 5]
match 첫번째요소(숫자들) {
    case Some(첫번째) => print("첫 번째 요소: {첫번째}")
    case None => print("배열이 비어있습니다")
}

Option 메서드 체이닝

// map으로 값 변환
let 결과 = 사용자찾기(1)
    .map(사용자 => 사용자.이름.toUpperCase())
    .map(이름 => "안녕하세요, {이름}님!")

match 결과 {
    case Some(인사) => print(인사)
    case None => print("사용자를 찾을 수 없습니다")
}

// andThen으로 Option 체이닝
function 성인확인(사용자: { 이름: string, 나이: int }) -> Option<string> {
    if 사용자.나이 >= 18 {
        return Some(사용자.이름)
    }
    return None
}

let 성인사용자 = 사용자찾기(1)
    .andThen(성인확인)
    .map(이름 => "{이름}님은 성인입니다")

// unwrapOr로 기본값 제공
let 사용자이름 = 사용자찾기(999)
    .map(사용자 => 사용자.이름)
    .unwrapOr("알 수 없는 사용자")

print("사용자 이름: {사용자이름}")  // "사용자 이름: 알 수 없는 사용자"

// filter로 조건 필터링
let 젊은사용자 = 사용자찾기(1)
    .filter(사용자 => 사용자.나이 < 30)
    .map(사용자 => "{사용자.이름}님은 젊습니다")

Option과 Result 조합

// Option을 Result로 변환
function 필수사용자찾기(아이디: int) -> Result<{ 이름: string, 나이: int }, string> {
    return 사용자찾기(아이디)
        .okOr("사용자를 찾을 수 없습니다: ID {아이디}")
}

// Result를 Option으로 변환
let 선택적결과 = 나누기(10, 0).ok()  // Ok는 Some으로, Error는 None으로

match 선택적결과 {
    case Some(값) => print("나누기 성공: {값}")
    case None => print("나누기 실패 (에러 메시지 무시)")
}

// 복합 처리
function 사용자나이두배(아이디: int) -> Result<int, string> {
    return 필수사용자찾기(아이디)
        .map(사용자 => 사용자.나이 * 2)
}

match 사용자나이두배(1) {
    case Ok(두배나이) => print("나이의 두 배: {두배나이}")
    case Err(오류) => print("오류: {오류}")
}

🔗 에러 전파와 조기 반환

? 연산자 (에러 전파)

// ? 연산자로 에러 자동 전파
function 복잡한계산(a: int, b: int, c: int) -> Result<float, string> {
    let 첫번째 = 나누기(a, b)?          // 에러시 자동 반환
    let 두번째 = 제곱근(첫번째)?         // 에러시 자동 반환
    let 최종 = 나누기(두번째, c)?        // 에러시 자동 반환
    
    return Ok(최종)
}

// 위 코드는 아래와 동일
function 복잡한계산_수동(a: int, b: int, c: int) -> Result<float, string> {
    let 첫번째 = match 나누기(a, b) {
        case Ok(값) =>
        case Err(오류) => return Err(오류)
    }
    
    let 두번째 = match 제곱근(첫번째) {
        case Ok(값) =>
        case Err(오류) => return Err(오류)
    }
    
    let 최종 = match 나누기(두번째, c) {
        case Ok(값) =>
        case Err(오류) => return Err(오류)
    }
    
    return Ok(최종)
}

// Option에서도 ? 사용 가능
function 첫번째와두번째합(배열: [int]) -> Option<int> {
    let 첫번째 = 첫번째요소(배열)?
    let 두번째 = 첫번째요소(배열.slice(1))?
    return Some(첫번째 + 두번째)
}

try 블록 (실험적)

// try 블록으로 여러 연산 묶기
function 데이터처리(파일경로: string) -> Result<{ 총합: int, 평균: float }, string> {
    try {
        let 내용 = 파일읽기(파일경로)?
        let 숫자들 = 내용.split(",")
            .map(s => parseInt(s.trim()))
            .collect()
        
        let 총합 = 숫자들.sum()
        let 평균 = 나누기(총합, 숫자들.length())?
        
        Ok({ 총합: 총합, 평균: 평균 })
    }
}

// 중첩된 try 처리
function 중첩처리() -> Result<string, string> {
    let 결과1 = try {
        let a = 나누기(10, 2)?
        let b = 제곱근(a)?
        Ok(b)
    }?
    
    let 결과2 = try {
        let c = 나누기(결과1, 3)?
        Ok(c * 2)
    }?
    
    return Ok("최종 결과: {결과2}")
}

defer (스코프 가드)

v4

현재 스코프가 종료될 때 실행할 정리 작업을 등록합니다. 실행 순서는 LIFO이며, 정상 종료/return/?에 의한 조기 반환/에러 모든 경로에서 보장됩니다.

정책: defer 블록 내부 오류는 기본적으로 로깅하고 전파하지 않습니다. 남은 defer 작업은 계속 실행됩니다.

function 파일처리(경로: string) -> Result<string, string> {
    let 파일 = File.open(경로)?
    defer { 파일.close() }                  // 항상 닫힘 (LIFO)

    let= 파일.lockExclusive()?          // 자원 획득
    defer { 락.release() }                  // 오류/반환 시에도 해제됨

    let 내용 = 파일.read()?                 // 자원 사용
    Ok(내용)
}

🎨 에러 처리 패턴

에러 컨텍스트 추가

// 에러에 컨텍스트 정보 추가
function 사용자데이터로드(아이디: int) -> Result<{ 이름: string, 설정: any }, string> {
    let 사용자 = 필수사용자찾기(아이디)
        .mapErr(오류 => "사용자 조회 실패 (ID: {아이디}): {오류}")?
    
    let 설정 = 설정파일읽기("user_{아이디}.json")
        .mapErr(오류 => "사용자 설정 로드 실패 (ID: {아이디}): {오류}")?
    
    return Ok({ 이름: 사용자.이름, 설정: 설정 })
}

// 단계별 에러 처리
function 단계별처리() -> Result<string, string> {
    print("1단계: 데이터 검증 중...")
    let 검증결과 = 데이터검증().mapErr(오류 => "1단계 실패: {오류}")?
    
    print("2단계: 데이터 변환 중...")
    let 변환결과 = 데이터변환(검증결과).mapErr(오류 => "2단계 실패: {오류}")?
    
    print("3단계: 데이터 저장 중...")
    let 저장결과 = 데이터저장(변환결과).mapErr(오류 => "3단계 실패: {오류}")?
    
    return Ok("모든 단계 완료: {저장결과}")
}

부분 실패 처리

// 여러 작업 중 일부 실패 허용
function 배치처리(파일목록: [string]) -> { 성공: [string], 실패: [{ 파일: string, 오류: string }] } {
    let mut 성공목록 = []
    let mut 실패목록 = []
    
    for 파일 in 파일목록 {
        match 파일처리(파일) {
            case Ok(결과) => {
                성공목록.push(결과)
                print("성공: {파일}")
            }
            case Err(오류) => {
                실패목록.push({ 파일: 파일, 오류: 오류 })
                print("실패: {파일} - {오류}")
            }
        }
    }
    
    return { 성공: 성공목록, 실패: 실패목록 }
}

// Result 배열 처리
function 모든결과수집<T, E>(결과들: [Result<T, E>]) -> Result<[T], [E]> {
    let mut 성공값들 = []
    let mut 오류들 = []
    
    for 결과 in 결과들 {
        match 결과 {
            case Ok(값) => 성공값들.push(값)
            case Err(오류) => 오류들.push(오류)
        }
    }
    
    if 오류들.length() > 0 {
        return Err(오류들)
    }
    
    return Ok(성공값들)
}

let 계산결과들 = [
    나누기(10, 2),   // Ok(5)
    나누기(8, 4),    // Ok(2)
    나누기(6, 0)     // Err("0으로 나눌 수 없습니다")
]

match 모든결과수집(계산결과들) {
    case Ok(값들) => print("모든 계산 성공: {값들}")
    case Err(오류들) => print("일부 계산 실패: {오류들}")
}

재시도 패턴

// 재시도 로직
function 재시도<T, E>(
    작업: function() -> Result<T, E>,
    최대시도: int,
    지연시간: int = 1000
) -> Result<T, E> {
    let mut 시도횟수 = 0
    
    while 시도횟수 < 최대시도 {
        시도횟수 += 1
        
        match 작업() {
            case Ok(결과) => {
                if 시도횟수 > 1 {
                    print("재시도 {시도횟수}번째에 성공")
                }
                return Ok(결과)
            }
            case Err(오류) => {
                print("시도 {시도횟수} 실패: {오류}")
                
                if 시도횟수 < 최대시도 {
                    print("{지연시간}ms 후 재시도...")
                    Thread.sleep(지연시간)
                } else {
                    return Err(오류)
                }
            }
        }
    }
    
    return Err("최대 재시도 횟수 초과")
}

// 네트워크 요청 재시도
let 응답 = 재시도(
    작업: () => 네트워크요청("https://api.example.com/data"),
    최대시도: 3,
    지연시간: 2000
)

match 응답 {
    case Ok(데이터) => print("데이터 수신: {데이터}")
    case Err(오류) => print("최종 실패: {오류}")
}

🛡️ 타입 안전성과 컴파일 시간 검사

모든 에러 케이스 처리 강제

// 컴파일러가 모든 케이스 처리를 강제함
enum 네트워크에러 {
    연결실패,
    시간초과,
    인증실패,
    서버오류(코드: int)
}

function 네트워크요청처리(결과: Result<string, 네트워크에러>) -> string {
    match 결과 {
        case Ok(데이터) => "성공: {데이터}"
        case Err(네트워크에러.연결실패) => "네트워크에 연결할 수 없습니다"
        case Err(네트워크에러.시간초과) => "요청이 시간 초과되었습니다"
        case Err(네트워크에러.인증실패) => "인증이 필요합니다"
        case Err(네트워크에러.서버오류(코드: 코드)) => "서버 오류 (코드: {코드})"
        // 모든 케이스를 처리하지 않으면 컴파일 에러!
    }
}

// Never 타입으로 불가능한 상황 표현
function 절대실패하지않음() -> Result<string, Never> {
    return Ok("항상 성공")
}

// Never는 매치할 필요 없음
let 결과 = 절대실패하지않음()
let= match 결과 {
    case Ok(값) =>
    // Error 케이스는 불가능하므로 생략 가능
}

커스텀 에러 트레이트

// 에러 정보를 풍부하게 만드는 트레이트
trait 에러정보 {
    function 메시지() -> string
    function 코드() -> int
    function 복구가능() -> bool
}

enum 애플리케이션에러: 에러정보 {
    데이터베이스연결실패,
    파일접근거부,
    잘못된입력(필드: string, 값: string),
    외부서비스불가능
}

impl 에러정보 for 애플리케이션에러 {
    function 메시지() -> string {
        match self {
            case 애플리케이션에러.데이터베이스연결실패 => "데이터베이스에 연결할 수 없습니다"
            case 애플리케이션에러.파일접근거부 => "파일에 접근할 권한이 없습니다"
            case 애플리케이션에러.잘못된입력(필드: 필드, 값: 값) => "잘못된 입력 - {필드}: {값}"
            case 애플리케이션에러.외부서비스불가능 => "외부 서비스를 사용할 수 없습니다"
        }
    }
    
    function 코드() -> int {
        match self {
            case 애플리케이션에러.데이터베이스연결실패 => 1001
            case 애플리케이션에러.파일접근거부 => 1002
            case 애플리케이션에러.잘못된입력(_, _) => 1003
            case 애플리케이션에러.외부서비스불가능 => 1004
        }
    }
    
    function 복구가능() -> bool {
        match self {
            case 애플리케이션에러.데이터베이스연결실패 => true
            case 애플리케이션에러.외부서비스불가능 => true
            case _ => false
        }
    }
}

// 에러 처리 통합
function 에러로깅(에러: 애플리케이션에러) {
    print("[에러 {에러.코드()}] {에러.메시지()}")
    
    if 에러.복구가능() {
        print("복구를 시도할 수 있습니다")
    } else {
        print("복구 불가능한 에러입니다")
    }
}

🎯 최적화와 베스트 프랙티스

1. 에러를 값으로 처리

// 좋은 예: 에러를 명시적으로 처리
function 안전한사용자입력() -> Result<int, string> {
    let 입력 = input("숫자를 입력하세요: ")
    
    match parseInt(입력) {
        case Ok(숫자) if 숫자 >= 0 => Ok(숫자)
        case Ok(_) => Err("음수는 허용되지 않습니다")
        case Err(_) => Err("유효한 숫자가 아닙니다: {입력}")
    }
}

// 나쁜 예: 예외 던지기 (Topaz에서는 지양)
function 위험한사용자입력() -> int {
    let 입력 = input("숫자를 입력하세요: ")
    let 숫자 = parseInt(입력).unwrap()  // 패닉 발생 가능!
    return 숫자
}

2. 에러 전파 최적화

// 효율적인 에러 전파
function 데이터파이프라인(입력: string) -> Result<string, string> {
    입력
        .trim()
        .nonEmpty().okOr("입력이 비어있습니다")?
        .validate().mapErr(오류 => "검증 실패: {오류}")?
        .transform().mapErr(오류 => "변환 실패: {오류}")?
        .process().mapErr(오류 => "처리 실패: {오류}")
}

// 조기 반환으로 깊은 중첩 방지
function 사용자등록(데이터: { 이름: string, 이메일: string, 나이: int }) -> Result<string, string> {
    // 조기 검증
    if 데이터.이름.length() == 0 {
        return Err("이름이 필요합니다")
    }
    
    if !데이터.이메일.contains("@") {
        return Err("유효한 이메일이 필요합니다")
    }
    
    if 데이터.나이 < 0 {
        return Err("유효한 나이가 필요합니다")
    }
    
    // 모든 검증 통과 후 처리
    let 사용자 = 사용자생성(데이터)?
    let 아이디 = 데이터베이스저장(사용자)?
    
    return Ok("사용자 등록 완료: {아이디}")
}

3. 에러 로깅과 모니터링

// 구조화된 에러 로깅
function 에러처리_로깅<T, E>(
    결과: Result<T, E>,
    컨텍스트: string,
    로그레벨: string = "error"
) -> Result<T, E> {
    match 결과 {
        case Ok(값) => {
            if 로그레벨 == "debug" {
                print("[DEBUG] {컨텍스트}: 성공")
            }
            return Ok(값)
        }
        case Err(오류) => {
            print("[{로그레벨.toUpperCase()}] {컨텍스트}: {오류}")
            
            // 메트릭 수집
            에러메트릭증가(컨텍스트, 로그레벨)
            
            return Err(오류)
        }
    }
}

// 사용 예시
let 결과 = 데이터베이스쿼리("SELECT * FROM users")
    |> 에러처리_로깅(컨텍스트: "사용자 목록 조회", 로그레벨: "error")

// 에러 집계 및 알림
function 에러알림시스템() {
    let 최근에러들 = 에러로그수집(지난시간: 3600)  // 1시간
    
    if 최근에러들.length() > 100 {
        알림발송("높은 에러율 감지: {최근에러들.length()}건/시간")
    }
    
    let 치명적에러들 = 최근에러들.filter(에러 => 에러.레벨 == "critical")
    if 치명적에러들.length() > 0 {
        긴급알림발송("치명적 에러 발생", 치명적에러들)
    }
}

토파즈의 에러 처리는 타입 안전하고 명시적입니다. Result와 Option을 활용하여 예측 가능하고 견고한 애플리케이션을 구축하세요! 🚀