건우의 개발 일기

Mobile CleanArchitecture Error Handling (Domain Error) 본문

고민연구

Mobile CleanArchitecture Error Handling (Domain Error)

거누팍 2022. 3. 3. 16:23

 

Clean Architecture 구조에 맞게 오류를 처리하는 방법에 대한 생각

 

👍 Domain Error

에러는 최종적으로 사용자에게 보이게 된다. 그렇기 때문에 개발자 혼자 생각하는 것이 아닌 기획자, 디자이너와의 커뮤니케이션을 통해 적합한 처리를 해주어야 한다.

이때 필요한 것이 보편 언어 (Ubiquitous language)이다. 예를 들어서 기획자에게 "Decode 에러는 어떤 식으로 처리할까요?"라고 말한다면 기획자는 쉽게 알 수 없을 것이다. 사용자도 마찬가지이다. Decode, Encode Error 등은 사용자에게도 불필요한 정보이다.

 

Clean Architecture 3 계층 중 비즈니스 로직을 포함하고 있는 Domain 계층은 Ubiquitous language를 담고 있다.

Domain 계층에 Error를 명세하는 것이 사용자 관점으로 에러를 명세할 수 있도록 하는 적합한 방법이다.

 

핵심은 위에 내용이고.. 아래 내용은 부가적인 생각..


✌️ Domain Error는 그룹화시켜야 할까?

Swift에서 에러를 핸들링하는 대표적인 방법은 Enum을 활용하여 에러를 그룹화하는 방법이다.

범위 내에 에러 케이스가 명세되기 때문에 처리할 때 switch 문을 통하여 놓치지 않을 수가 있다.

 

그렇다면 UseCase 마다 Domain Error를 만들어 UseCase 가 발생시킬 수 있는 에러를 그룹화하는 것이 처리하기에 좋지 않을까?

 

// Domain

enum LoginError: Error {
    case idError(message: String, detailMessage: String)
    case pwError(message: String, detailMessage: String)
}

class LoginUseCase: LoginUseCaseProtocol {
    private let loginRepository: LoginRepositoryProtocol
    
    init(loginRepository: LoginRepositoryProtocol) {
        self.loginRepository = loginRepository
    }
    
    func excute(request: LoginUseCaseRequest) -> Observable<LoginUseCaseResponse> {
        //
    }
}

// Data

class LoginRepository: LoginRepositoryProtocol {
    func login(loginData: LoginData) -> Observable<LoginInfo> {
        return .error(LoginError.idError(message: "", detailMessage: "")
    }
}

로그인의 간단한 예제 코드이다. idError와 pwError라는 Domain Error를 LoginError로 그룹화시켜 관리하도록 하였다.

Repository에서 Domain Model을 Return 하듯 Domain Error 또한 발생시킬 수 있다.

class FlowControlRepository {
    func control() -> Observable<Void> {
        //
    }
}

하지만 이때 동시 접속자 수 관리 로직이 추가되었다. LoginUseCase에서 이 함수를 호출하여 로그인 통신 전에 수행하게 된다.

그렇다면 여기에서는 어떤 Domain Error를 throw 해줄 것인가?

 

대표적으로 세 가지 방법이 있을 것이다.

 

  1. LoginError에 flowControl 관련 에러 케이스를 추가하여 throw 한다.
    • 동시 접속자 수 관리 통신 로직이 다른 UseCase에서도 사용된다면 Login에 종속시킬 수 없게 된다.
  2. FlowControlError (Domain Error)를 만들어 throw 한다.
    • 에러를 그룹화하는 이유 중 하나가 "범위 내에 에러 케이스가 명세되기 때문에 처리할 때 놓치지 않을 수 있다."이다. 하지만 로그인 시에 발생할 수 있는 Domain Error 그룹이 두 가지 존재하게 된다면 그룹화하는 이유가 없어지게 된다.
  3. Repository 계층에서 발생시킬 수 있는 에러 그룹을 따로 만들어 throw 하고 Mapper를 통해 Domain Error로 변환한다.
    • Mapper를 통하여 계층 간의 원활한 분리가 이루어지게 된다. 하지만 Repository 계층에서 발생하는 에러 그룹을 명세하는 비용이 상당히 많이 든다. 범위를 정확히 지정하기 위해선 Repository 함수 하나당 Enum 이 존재해야 하기 때문이다.
    • 계층이 더 추가되거나 Repository에서 다른 Repository를 호출하였을 때에도 Enum과 Enum의 매핑 로직이 매번 발생한다
    • 그렇기 때문에 그룹화를 시키지 않고 Domain Error 자체를 throw 하는 방법을 생각해보았다.

 

👌 Domain Error와 ErrorCase

public struct IDError: Error,
                       HasMessage,
                       HasDetailMessage {
    
    public var message: String
    
    public var detailMessage: String
}

우선 Domain Error 자체는 그룹화시키지 않았다. Domain Model처럼 하나의 struct 형태로 존재하게 된다.

 

그리고 이 Domain Error를 여러 필요한 정보를 담아 throw 하게 된다.

 

하지만 이렇게 되면 Presentation에서 에러를 받을 때 범위를 특정하지 못하게 된다. UseCase에서 발생할 수 있는 에러 그룹 형태로 Return 하는 것이 에러 처리에는 효과적일 것이다.

 

그래서 UseCase 마다 발생할 수 있는 에러 그룹을 명세하여 마지막에 해당 형태로 매핑하는 것이 효과적일 것이라 생각했다.

 

public enum LoginErrorCase: Error {
    case idError (IDError)
    case passwordError (PasswordError)
}

public extension Error {
    func toLoginError() -> LoginErrorCase? {
        if let error = self as? IDError {
            return .idError(error)
        }
        if let error = self as? PasswordError {
            return .passwordError(error)
        }
        return nil
    }
}

위의 로직은 Domain 계층에 존재하게 된다. 각 UseCase에서 발생될 수 있는 Domain Error 명세는 비즈니스라고 볼 수 있기 때문이다.

 

이렇게 되면 자유롭게 Domain Error를 발생시킬 수 있고, 처리할 때에도 이 기능에서 나올 수 있는 모든 에러 케이스가 명세되었기 때문에 놓치지 않을 수 있다.


😀 마무리

 

Clean Architecture, 도메인 주도 설계에 맞게 에러를 명세하는 방법에 대해서 생각해보았다.

도메인 계층을 통한 비즈니스 로직 설계는 주요 로직을 외부적인 요인과 분리시키는 효과도 있지만 보편언어를 통한 기획자와의 원활한 커뮤니케이션 또한 가능하게 한다. 이런식으로 에러를 명세하게 되었을때 도메인 에러는 기획자와의 커뮤니케이션 브릿지로 작용되어 사용자에게 더욱 알맞는 에러 처리를 보여줄 수 있을것이다.