에러 처리는 견고한 소프트웨어의 핵심입니다. 토파즈는 타입 안전한 에러 처리를 위해 Result와 Option 타입을 제공합니다. 🛡️
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을 활용하여 예측 가능하고 견고한 애플리케이션을 구축하세요! 🚀