Skip to content

예외 핸들링 예외 표시 통일

전종현 edited this page Jun 9, 2024 · 3 revisions

문제 상황

  • 예외 처리 방식을 각 도메인 마다 통일 해야 할 필요가 있었다.

  • 프레젠테이션 계층에서도 데이터 계층에서 사용한 외부 의존성이 예외를 통해 그대로 전달되었다.

    ex) Firebase 패키지 의존성 - } on FirebaseException catch (e) {

  • 스낵바를 통해 예외를 표시하는 것은 모든 화면에서 동일하게 존재하는 기능으로 중복되는 코드가 발생하였다.

  • 데이터 계층에서 발생한 예외를 잡아서 핸들링 하는 곳이 View 또는 ViewModel로 일관성이 없었다.

  • 사용자에게 알려야 할 오류 일 경우, 발생한 곳에서 UI 단으로 throw하여 처리 할 필요가 있다고 판단했다.


구현 및 해결

1. DataSource 예외 처리 및 통일

  • 예외 처리 통일을 위해, DataSource에서 잡은(catch) 예외를 커스텀 예외(ExceptionCode)로 변경하여 던지도록(throw) 처리하였다.

    class RemoteFileDataSourceImpl implements RemoteFileDataSource {
      final FirebaseStorage _firebaseStorage;
      final FirebaseAuthService _firebaseAuthService;
    	
    	...
    	
      @override
      Future<List<String>> saveImage(
          {required File croppedImage, required File compressedImage}) async {
        try {
    	     ... // 이미지 요청 로직
        } catch (e) {
          throw switch (e) {
    	      // 발생한 예외를 타입에 따라 직접 구현한 ExceptionCode 클래스로 매핑
            FirebaseException _ => ExceptionCode.internalServerException,
            DioException _ => ExceptionCode.networkException,
            _ => e,
          };
        }
      }
    }

2. 예외 처리(catch) 및 예외 상태 데이터 바인딩 통일

  • 유저에게 표시되어야 할 예외 처리를 통일하기 위해, Base 추상 클래스를 작성하여 각 뷰 모델(BaseChangeNotifer)과 뷰(BaseState)에 일괄 적용하였다.

  • 뷰모델에서(BaseChangeNotifer) 유스케이스 요청에 대한 예외를 잡아서, 예외 상태를 나타내는 exceptionHolder에 전달하도록 하였다.

    abstract class BaseChangeNotifier extends ChangeNotifier {
      // Stream
      // final _exceptionController = StreamController<ExceptionStatus>();
      ExceptionStatus? _exceptionHolder;
    
      ExceptionStatus? get exceptionHolder => _exceptionHolder;
    
    	// 뷰에서 예외 처리 후, 상태 초기화
      void exceptionHandled() {
        _exceptionHolder = null;
      }
    
      // Exception -> Stream 전송
      void notifyException({required Object? exception}) {
        _exceptionHolder = exceptionToExceptionStatus(exception: exception);
      }
    
      // Exception -> ExceptionStatus 매핑
    ExceptionStatus exceptionToExceptionStatus({required Object? exception}) {
        return switch (exception) {  
            ExceptionCode _ => ExceptionStatus(message: (exception.errorMessage)),
        ...
        _ => ExceptionStatus(message: ('알 수 없는 오류가 발생하였습니다'), exceptionAlertType: ExceptionAlertType.snackBar),
      };
    }
    
    // 구현체에서 사용 예시
      /// HomeScreenViewModel 배경 이미지 주소 가져오기
      Future<void> _getBackgroundImagePath() async {
        try {
          final weatherBackgroundImageList =
          await getBackgroundImageListUseCase.execute();
    
          if (weatherBackgroundImageList.isEmpty) {
            _weatherBackgroundImage = ImagePath.homeBackgroundSunny;
          }
    
          WeatherBackgroundImage image = weatherBackgroundImageList.firstWhere(
                  (element) =>
              element.code ==
                  WeatherCode.fromValue(currentWeather!.code).value);
    
          _weatherBackgroundImage = image.imagePath;
        } catch (e) {
    	    // 예외 상태(exceptionHolder)로 전달
          notifyException(exception: e);
        }
      }
    
  • 뷰(BaseState)에서 바인딩하고 있던 exceptionHolder의 값이 변경되면, 에러 메세지를 스낵바로 표시하도록 처리하였다.

    abstract class BaseState<S extends StatefulWidget, V extends BaseChangeNotifier> extends State<S> {
    
      @override
      void didChangeDependencies() {
        super.didChangeDependencies();
        final ExceptionStatus? exceptionStatus = context.read<V>().exceptionHolder;
        if (exceptionStatus == null) return;
        context.read<V>().exceptionHandled();
        Future.microtask(() {
    	    // 예외 상태(exceptionHolder)를 스낵바로 표시
          ExceptionAlertHandler.showAlert(
            context: context,
            message: exceptionStatus.message,
          );
        });
      }
    }

결론 (느낀점)

  • 특수 상황에 대한 처리가 예외로 바뀌면서 메서드의 동작이 명확해지고 코드의 가독성이 좋아졌다.
  • 추상 클래스 구현을 통해, 뷰에서 예외 상태를 표시하는 보일러 플레이트 코드가 사라졌다.
  • 중복되는 코드가 사라졌다.
  • exception 핸들링(try-catch-finally) 방식에 대해 깊게 고민해 볼 수 있는 계기가 되었다.
  • 데이터 소스 단에서 발생 가능한 다양한 예외 케이스와 그 중에서 유저에게 알려야 할 예외는 무엇인지에 대해 고민하게 되었다.