Result Type: 개념, Result 컨테이너 타입 및 모바일 개발에서의 작동 방식

저자: IT Sectr 게시일: 2026-05-26 읽는 시간: 9 분

Result Type — 성공(Success) 또는 실패(Failure)로 완료될 수 있는 작업의 결과를 나타내는 컨테이너 타입입니다. 예외와 달리 Result는 스택 풀기 없이 오류를 일반 값으로 전달하므로 예상되는 오류 처리를 더 안전하고 합성 가능하게 만듭니다. Apple Swift Documentation (2026)에 따르면, Swift의 Result<Success, Failure>는 프로그램 실행을 중단하지 않고 map과 flatMap을 통한 자동 오류 처리로 작업을 연결할 수 있습니다.

핵심 사항

  • Result Type — 두 가지 상태(Success(데이터) 및 Failure(오류))를 가진 제네릭 컨테이너, 스택 풀기 없음
  • Swift에서 Result<Success, Failure> — map, flatMap, mapError 및 get 메서드가 있는 내장 타입
  • Kotlin에서 Result<T>는 성공 또는 Throwable을 나타내며 getOrNull, getOrDefault, fold 함수 제공
  • Dart에서 표준 Result<T>는 Dart 3.0부터 사용 가능, fpdart 패키지의 Either도 사용 가능
  • 구성 map과 flatMap을 통해 Result를 반환하는 여러 작업을 중첩 검사 없이 결합 가능

Result Type이란?

Result Type은 성공하거나 실패할 수 있는 작업의 결과를 캡슐화하는 제네릭 타입입니다. 오류가 정상 흐름을 중단하고 catch 블록을 찾기 위해 스택 풀기가 필요한 예외와 달리, Result는 오류를 일반 값으로 전달합니다 — 호출자는 항상 객체를 받고 처리 방법을 결정합니다. 이는 특히 예상되는 오류(잘못된 입력, 비즈니스 규칙, 서버 거부)에 유용하며, 이러한 경우 예외는 너무 무거운 메커니즘입니다.

Result의 개념은 함수형 프로그래밍에서 유래했으며, 유사한 타입을 Either(또는 Left/Right)라고 합니다. Swift에서 Result는 Swift 5.0에서 표준 타입이 되었습니다. Kotlin에서는 Result<T>가 표준 라이브러리에 추가되었고, Dart 3.0에서는 내장 Result<T>가 도입되었습니다. 각 구현은 컨테이너 작업을 위한 메서드를 제공합니다: map(성공 값 변환), flatMap(Result 함수 연결), mapError(오류 변환), fold(두 경우 모두 처리). Result Type은 모바일 개발에서 함수형 오류 처리의 초석이며, 예상 시나리오에 대한 try-catch의 대안입니다.

Result의 주요 장점은 합성 가능성입니다. 각각 실패할 수 있는 여러 작업을 중첩된 try-catch 블록 없이 하나의 체인으로 결합할 수 있습니다. 체인의 어떤 작업이 Failure로 실패하면 전체 체인이 중단되고 Failure를 반환합니다 — 단일 if 조건이나 catch 블록 없이. 이는 특히 여러 순차적 API 요청이나 비즈니스 규칙 검사 시나리오에서 코드를 선형적이고 읽기 쉽게 만듭니다.

Swift의 Result

Swift에서 Result<Success, Failure>는 두 가지 케이스가 있는 enum입니다: .success(Success)와 .failure(Failure). Failure는 Error 프로토콜에 의해 제한됩니다. Swift Result는 map, flatMap, mapError 및 get 메서드를 갖춘 완전한 함수형 타입입니다. get은 특별한 메서드로, 결과가 성공이면 Success 값을 반환하고 Failure면 오류를 throw합니다. 이를 통해 Result는 함수형 스타일과 예외 기반 스타일 간의 브리지 역할을 할 수 있습니다: 체인에서 map/flatMap으로 처리하고 마지막에 do-catch와 함께 get을 사용하여 throws 코드와 통합합니다.

Result enum과 철저한 switch

Swift Result는 제네릭 매개변수가 있는 enum으로, 컴파일러가 switch나 do-catch를 통해 철저한 처리를 강제할 수 있습니다. NetworkError enum에 새 케이스를 추가하면, 해당 케이스가 처리되지 않은 모든 switch 표현식에서 컴파일러가 오류를 생성합니다. 철저한 검사는 예외에 비해 Result의 주요 장점입니다: 컴파일러는 빌드 시점에 모든 가능한 오류가 고려되었음을 보장합니다. 대조적으로 Swift에서 예외는 컴파일러에 의해 검사되지 않습니다(throws만 선언되고 오류 타입은 선언되지 않음).

swift
enum NetworkError: Error {
    case badURL
    case requestFailed(String)
    case decodingFailed
}

func fetchUser(id: Int) -> Result<User, NetworkError> {
    guard let url = URL(string: "https://api.example.com/users/\(id)") else {
        return .failure(.badURL)
    }
    let result = performRequest(url: url)
    switch result {
    case let .success(data):
        if let user = try? JSONDecoder().decode(User.self, from: data) {
            return .success(user)
        }
        return .failure(.decodingFailed)
    case let .failure(error):
        return .failure(.requestFailed(error.localizedDescription))
    }
}

// switch와 함께 사용
let result = fetchUser(id: 42)
switch result {
case .success(let user):
    showUser(user)
case .failure(.badURL):
    logError("Invalid URL")
case .failure(.requestFailed(let msg)):
    showAlert(msg)
case .failure(.decodingFailed):
    logError("Decoding error")
}

Result<Success, Failure>는 타입 수준에서 오류를 타입화할 수 있습니다: fetchUser 함수는 Result<User, NetworkError>를 반환하며, NetworkError는 세 가지 케이스가 있는 구체적인 enum입니다. 호출자는 철저한 커버리지로 switch를 통해 각 케이스를 처리합니다 — 컴파일러는 모든 변형이 처리되었는지 확인합니다. 철저한 switch는 예외에 비해 Result의 핵심 장점입니다: 컴파일러는 .badURL, .requestFailed 또는 .decodingFailed 처리를 잊지 않도록 보장합니다. 예외의 경우 컴파일러가 처리를 요구하지 않으며, 코드 리뷰에서 누락된 catch(.badURL)를 간과하기 쉽습니다.

Kotlin의 Result

Kotlin에서 Result<T>는 표준 라이브러리의 내장 타입으로 성공(T) 또는 오류(Throwable)를 나타냅니다. Swift와 달리 Kotlin Result는 구체적인 오류 타입을 지정할 수 없습니다 — Throwable만 가능합니다. 이는 단순성을 위해 설계되었지만 when 표현식을 통한 추가 오류 타입 검사가 필요합니다. Kotlin Result는 fold(두 경우 처리), getOrNull(성공 또는 null), getOrDefault(성공 또는 기본값), map, recover, andThen 함수를 지원합니다. Kotlin의 특징: Result는 함수 경계를 넘어 직접 전파되도록 설계되지 않았습니다 — 추가 적응 없이 Android SDK 메서드나 Kotlin Coroutines API의 반환 타입으로 사용할 수 없습니다.

kotlin
fun parseJson(input: String): Result<JsonObject> {
    return runCatching {
        JsonParser.parseString(input).asJsonObject()
    }
}

fun validateEmail(email: String): Result<String> {
    return if (email.contains("@")) {
        Result.success(email.trim())
    } else {
        Result.failure(IllegalArgumentException("Invalid email"))
    }
}

data class SignupData(val name: String, val email: String)

fun processSignup(name: String, email: String): SignupResult {
    return validateEmail(email).fold(
        onSuccess = { validateName(name) },
        onFailure = { SignupResult.Error("Invalid email") }
    )
}

runCatching은 Kotlin의 편리한 래퍼로, 모든 예외를 캐치하여 Result.failure를 반환합니다. validateEmail은 검증에 따라 Result.success 또는 Result.failure를 반환합니다. fold는 두 경우를 간결하게 처리합니다: Success 시 다음 검증 함수가 호출되고, Failure 시 SignupResult.Error가 반환됩니다. 중요: Kotlin Result는 data class 필드에 저장하거나 suspend 함수 경계를 넘어 직접 전송하도록 설계되지 않았습니다 — Jetpack Compose 또는 MVVM에서 UI 상태를 표현하려면 커스텀 sealed class(Success/Error/Loading)를 사용하세요.

Dart 및 Flutter의 Result

Dart 3.0부터 표준 라이브러리에 내장 Result<T>가 포함되었습니다 — 두 개의 생성자가 있는 sealed class입니다: T.ok()(성공)와 Error.error()(Object 및 StackTrace를 포함한 오류). Dart 3.0 이전에는 Flutter 개발자가 dartz 패키지의 Either<L, R>이나 커스텀 sealed class를 사용했습니다. Dart의 내장 Result는 최소한으로, map/flatMap을 직접 제공하지 않습니다 — 이러한 함수는 when을 사용하거나 확장 메서드를 통해 구현해야 합니다. 진지한 함수형 처리를 위해 fpdart의 Either가 map, flatMap, mapLeft, fold, andThen 및 bind 연산자를 지원하는 더 강력한 솔루션입니다.

fpdart의 Either

Either<L, R>은 fpdart 패키지의 왼쪽 편향 타입으로, Left는 오류, Right는 성공입니다. 내장 Result<T>와 달리 Either는 타입 매개변수 수준(L)에서 오류를 타입화하여 컴파일 시점에 오류 타입 구분을 가능하게 합니다. fpdart 패키지는 함수형 컴비네이터의 완전한 세트를 제공합니다: map(Right -> Right), mapLeft(Left -> Left), flatMap(bind — 중첩 Either), andThen(변환 없는 체인), fold(Either 탈출), getOrElse(기본값). 함수형 접근 방식을 사용하는 Flutter 애플리케이션에서 Either는 사실상의 표준입니다.

dart
import 'dart:convert';
import 'package:fpdart/fpdart.dart';

class UserService {
    Either<AppError, User> fetchUser(String id) {
        try {
            final response = await http.get(
                Uri.parse('https://api.example.com/users/$id')
            );
            if (response.statusCode == 200) {
                final user = User.fromJson(
                    json.decode(response.body)
                );
                return Either.of(user);
            }
            return Either.left(
                AppError.serverError(response.statusCode)
            );
        } on SocketException catch (e) {
            return Either.left(AppError.networkError(e.message));
        }
    }
}

// fold와 함께 사용
final result = await service.fetchUser('42');
result.fold(
    (left)  => showError(left.message),
    (right) => showUser(right),
);

Either<L, R>(fpdart)는 왼쪽 편향 타입입니다: Left — 오류, Right — 성공. fetchUser는 Either<AppError, User>를 반환하며, AppError는 구체적인 오류 타입(serverError, networkError)을 가진 sealed class입니다. fold는 두 경우를 처리합니다: 첫 번째 콜백은 Left(오류), 두 번째는 Right(성공)입니다. Dart 3.0에서 내장 Result도 fold를 지원하지만 map/flatMap은 제공하지 않습니다. 체인의 경우 fpdart의 Either가 map, flatMap(bind), mapLeft, andThen을 제공합니다 — 오류 구성을 위한 함수형 컴비네이터의 완전한 세트입니다.

map과 flatMap을 통한 Result 구성

예외에 비해 Result의 주요 장점은 구성입니다. 각각 실패할 수 있는 여러 작업이 있는 경우, 중첩된 if나 try-catch 없이 map과 flatMap을 사용하여 체인으로 연결할 수 있습니다. map은 성공 값을 변환합니다: Result.success(x) -> Result.success(f(x)). flatMap(bind 또는 andThen이라고도 함)은 변환 자체가 Result를 반환하는 경우에 사용합니다: Result.success(x) -> f(x) -> Result<Y>. 어떤 단계가 Failure로 실패하면 후속 작업이 실행되지 않습니다 — 체인이 중단됩니다.

kotlin
data class UserRequest(val userId: String, val token: String)

sealed class AuthError {
    data object InvalidToken : AuthError()
    data class UserNotFound(val id: String) : AuthError()
}

typealias Outcome<T> = Either<AuthError, T>

fun validateToken(token: String): Outcome<String> =
    if (token.isNotBlank()) Either.right(token)
    else Either.left(AuthError.InvalidToken)

fun fetchProfile(userId: String): Outcome<Profile> =
    if (userId == "42") Either.right(Profile("Alice"))
    else Either.left(AuthError.UserNotFound(userId))

// flatMap을 통한 구성(fpdart의 andThen)
val result = validateToken("abc123")
    .flatMap { fetchProfile("42") }
    .map { it.name }
    .getOrElse { "Guest" }

println(result) // "Alice"

체인: validateToken -> fetchProfile -> map name -> getOrElse “손님”. validateToken이 Left(InvalidToken)를 반환하면 체인이 중단되고 “손님”을 반환합니다. fetchProfile이 Left(UserNotFound)를 반환해도 “손님”입니다. 두 작업이 모두 성공하면 프로필 이름이 반환됩니다. flatMap은 각각 실패할 수 있는 Either 함수를 하나의 선형 체인으로 결합할 수 있습니다. 전통적인 예외 스타일에서는 동일한 코드에 두 개의 중첩된 try-catch나 null 검사가 필요합니다. 마지막의 getOrElse는 구성의 종료 지점으로, Failure 케이스의 기본값을 제공합니다.

Result vs 예외: 상황별 선택 가이드

Result와 예외는 상호 배타적인 접근 방식이 아닙니다. 각각 고유한 영역이 있으며, 잘 설계된 모바일 애플리케이션에서는 둘 다 사용됩니다. 선택은 오류가 예상되는지 아니면 예상치 못한지에 따라 달라집니다. Result는 비즈니스 로직의 일부인 예상 오류용입니다: 잘못된 이메일, 잔액 부족, 요청 한도 초과. 예외는 예상치 못한 시스템 오류용입니다: 네트워크 손실, OutOfMemoryError, NullPointerException(발생해서는 안 되지만 발생하는 것).

기준Result Type예외(Exception/Error)
오류 유형예상됨(비즈니스 로직)예상치 못함(시스템)
성능낮은 비용(스택 풀기 없음)높은 비용(스택 풀기, StackTrace 캡처)
구성map/flatMap을 통한 선형 체인중첩된 try-catch — 읽기 어려움
컴파일러철저한 검사(switch/when)Java의 checked exceptions만
실행 흐름중단되지 않음 — 오류를 값으로가장 가까운 catch까지 중단됨
테스트쉬움: 결과 확인, isSuccess/isError 단언assertThrows 및 목 객체 필요
사용 시기비즈니스 검증, 요청 체인, 폼네트워크 손실, I/O 오류, 시스템 충돌

실용적인 규칙: 오류가 애플리케이션의 정상적인 워크플로의 일부(사용자가 잘못된 이메일 입력, 권한 부족)인 경우 Result를 사용합니다. 오류가 예외적인 상황(서버 응답 없음, 메모리 부족)인 경우 예외를 사용합니다. 모바일 개발에서 레이어 경계에서의 Result(UseCase -> ViewModel)와 레이어 내부의 예외(API -> Repository)는 두 접근 방식의 장점을 결합한 일반적인 패턴입니다.

예외에서 Result로의 마이그레이션 전략

기존 예외 기반 코드를 Result로 마이그레이션하는 것은 점진적으로 이루어져야 합니다. 레이어 경계부터 시작하세요: throws 함수 호출을 Result { try ... }(Swift) 또는 runCatching { ... }(Kotlin)으로 래핑합니다. 그런 다음 Repository 및 UseCase 메서드의 반환 타입을 Result/Either로 교체하고 내부 구현은 예외로 유지합니다. 마지막 단계에서 ViewModel을 마이그레이션합니다: 예외가 있는 UiState 대신 sealed class UiState<T>(Loading, Success, Error)를 사용하고, Error는 Throwable이 아닌 도메인 오류를 저장합니다. 점진적 마이그레이션을 통해 전역 리팩토링 없이 각 레이어를 개별적으로 테스트할 수 있습니다.

자주 묻는 질문

Result Type과 Optional/Option의 차이점은?

Optional(T?)은 값의 존재 또는 부재를 나타냅니다 — nil은 “데이터 없음”을 의미하지만 이유를 설명하지 않습니다. Result(Success/Failure)는 성공뿐만 아니라 구체적인 타입의 오류 이유도 포함합니다. Optional은 부재가 정상적인 경우(예: 선택적 프로필 필드)에 사용하고, Result는 오류 정보가 필요한 경우에 사용합니다.

throws 함수를 Result로 변환하는 방법은?

Swift에서는 Result { try throwingFunc() }를 사용합니다 — Result 생성자는 throws 클로저를 받습니다. Kotlin에서는 runCatching { throwingFunc() }을 사용하며 Result<T>를 반환합니다. Dart에서는 Result<T>.tryCatch(() => throwingFunc())를 사용합니다. 이를 통해 예외 기반 코드를 Result 체인에 쉽게 통합할 수 있습니다.

정보 손실 없이 Result에서 오류를 처리하는 방법은?

성공 값을 변경하지 않고 오류 타입을 변환하려면 mapError(Swift) 또는 mapLeft(Dart/Kotlin의 Either)를 사용합니다. 두 경우를 모두 처리하고 단일 값을 반환해야 하는 경우 fold를 사용합니다. 체인을 중단하지 않고 로깅하려면 onFailure(Kotlin) 또는 접두사 검사 지점을 사용합니다.

Kotlin에서 Result를 코루틴과 함께 사용할 수 있나요?

네, 하지만 주의가 필요합니다. Kotlin Result<T>는 K2 컴파일러 및 리플렉션 특성으로 인해 suspend 함수의 반환 타입으로 직접 권장되지 않습니다. 코루틴에서 상태를 표현하려면 자체 sealed class NetworkResult<T>(Success, Error, Loading)를 사용하세요. 비즈니스 로직의 예상 오류에는 Arrow의 Either가 더 강력한 대안입니다.

Result 컨텍스트에서 fold란?

fold는 두 개의 콜백(onSuccess와 onFailure)을 받아 모든 타입의 단일 값을 반환하는 메서드입니다. switch 표현식과 동등하지만 고차 함수 형태입니다. fold는 Result 체인의 주요 종료 지점으로, Success/Failure를 UiState, 사용자 대상 문자열 또는 다른 Result로 변환합니다.

요약

  • Result Type — 스택 풀기 없이 예상 오류를 안전하게 처리하기 위한 제네릭 컨테이너(Success/Failure)
  • Swift에서 Result<Success, Failure>(Failure: Error)는 타입 안전 오류와 함께 switch를 통한 철저한 검사 제공
  • Kotlin에서 Result<T>는 성공 또는 Throwable을 래핑, runCatching은 throws 코드에서 편리한 생성자
  • Dart에서 내장 Result<T>(Dart 3.0) 및 fpdart의 Either<L, R>로 map/flatMap을 통한 고급 구성
  • 구성 map(성공 변환)과 flatMap(Result 함수 연결)을 통해 중첩된 try-catch 대체
  • Result vs 예외: 예상된 비즈니스 오류에는 Result, 예상치 못한 시스템 장애에는 예외
  • 실행 흐름을 중단하지 않는 명시적이고 테스트 가능한 오류 처리를 위해 레이어 경계에서 Result 사용

턴키 방식의 모바일 애플리케이션을 개발해 드립니다

IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.

프로젝트 논의

더 읽어보기