ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [디자인패턴 변환 코드 리팩토링- 클린아키텍쳐가 필요한 이유]
    취준(자소서,면접)/프로젝트 정리 2024. 8. 19. 10:40

     

         [ MVVM과 클린아키텍쳐의 차이 ]

     

         1. 의존성역전원칙

         1) MVVM은 여전히 상위계층인 ViewModel이 하위계층인 Service와 Repository에 직접 의존

           API가 변경되면 Service뿐만 아니라 ViewModel도 수정이 필요할 수 있다

     

        [예제1] 종종 요청이 오는 에러코드 세분화를 예제로 살펴보자.

     

      * 기존에는 에러코드를 하나로 엮어서 처리함

    // 1. Model 정의
    ///  이체한도금액 조회
    public struct AC4192Q1OutRec: Codable {
      /// 등록해지구분코드-1.등록2.해지3.변경 - 1
      public var RGST_CCLC_SCD: String = ""
      /// 1회이체한도금액 - 19
      public var TM1_TRNF_LMT_AMT: String = ""
      /// 1일이체한도금액 - 19
      public var DY1_TRNF_LMT_AMT: String = ""
      /// 1회소액이체한도금액 - 19
      public var TM1_SAMT_TRNF_LMT_AMT: String = ""
      /// 1일소액이체한도금액 - 19
      public var DY1_SAMT_TRNF_LMT_AMT: String = ""
      /// 조회모드출금개별계좌번호-해지된경우빈값 - 11
      public var QTTN_MD_DRWG_IACN: String = ""
    
      public init() {}
    }
    
    
    // 2. Service에서 실제 호출 로직 구현
    final class ACService {
        /// 이체한도금액조회
        func sendAC4192Q1(IACN: String, completion: @escaping(AC4192Q1OutRec?, Bool) -> Void) {
            var otr = OTR_SEND(name: "AC4192Q1")
            otr?.setValue(IACN, forKey: "IACN")
            
            // 단순에러처리
            Network.shared().request(with: otr) { sender, isError, msg in
                if isError {
                    completion(nil, true) // 에러는 무조건 서버에러라고 가정함
                } else {
                    let output = sender as? AC4192Q1OutRec
                    completion(output, false)
                }
            }
        }
    }
    
    
    // 3. Repository에서 비즈니스 로직 구현
    
    class ACRepository {
        private let acService: ACService
    
        init(acService: ACService) {
            self.acService = acService
        }
        
        func fetchTransferLimitAmount(completion: @escaping(AC4192Q1OutRec?, Bool) -> Void) {
        
            acService.sendAC4192Q1(IACN: "") { result in
                completion(result)
            }
            
        }
    }
    
    
    
    // ViewModel
    class TransferViewModel {
        private let acRepository: ACRepository
    
        init(acRepository: ACRepository) {
            self.acRepository = acRepository
        }
    
        func loadTransferLimitAmount() {
    
            acRepository.fetchTransferLimitAmount { output, isError in
                UILabel().text = output?.DY1_SAMT_TRNF_LMT_AMT
                UILabel().text = output?.TM1_SAMT_TRNF_LMT_AMT
                
                if isError {
                    HWAlertView.alert("Error !!!!")
                }
            }
        }
        
    }

     

     

     이 구조에서 만약 sendAC4192Q1를 반환할때 에러처리를 구체적인 방식으로 변경했다고 가정해보자.

    (에러에 따라 UI에 각기 다른 알럿을 띄움)

    final class ACService {
        func sendAC4192Q1(IACN: String, completion: @escaping(Result<AC4192Q1OutRec, TransferLimitError>) -> Void) {
            
            var otr = OTR_SEND(name: "AC4192Q1")
            otr?.setValue(IACN, forKey: "IACN")
            
            // 네트워크 요청 코드
            Network.shared().request(with: otr) { sender, isError, msg in
                if isError {
                    let errorCode = otr?.nextHeader.msg_COD
                    if errorCode == "aaa" {
                        completion(.failure(TransferLimitError.networkUnavailable))
                    } else if (errorCode == "bbb") {
                        completion(.failure(TransferLimitError.invalidResponse))
                    } // ...
          
                } else {
                    let output = sender as? AC4192Q1OutRec
                    completion(.success(output))
                }
            }
        }
    }
    
    
    // ACRepository도 서비스 레이어의 변경에 따라 수정이 필요
    
    class ACRepository {
        private let acService: ACService
    
        init(acService: ACService) {
            self.acService = acService
        }
        
        func fetchTransferLimitAmount(completion: @escaping(Result<AC4192Q1OutRec, TransferLimitError>) -> Void) {
            acService.sendAC4192Q1(IACN: "") { result in
                completion(result)
            }
        }
    }

     

    * 오류 유형이 구체화되면서 ViewModel에서의 처리도 변경되어야 한다 >> 의존성역전원칙 위배

    class TransferViewModel {
       private let acRepository: ACRepository
    
       init(acRepository: ACRepository) {
           self.acRepository = acRepository
       }
    
       func loadTransferLimitAmount() {
           acRepository.fetchTransferLimitAmount { result in
               switch result {
               case .success(let output):
                   UILabel().text = output.TM1_TRNF_LMT_AMT
                   UILabel().text = output.DY1_SAMT_TRNF_LMT_AMT
                   
               case .failure(let error):
                   // 오류에 따라 알럿창 세분화
                   switch error {
                   case .networkUnavailable:
                       HMAlertView.alertLeft("Server error occurred.") { alert, index in
                           if (index == 0) {
                               // 확인버튼 클릭 시 뒤로가기 처리
                           }
                       }
                   case .serverError:
                       print("Server error occurred.")
                   case .invalidResponse:
                       print("Invalid response received.")
                   }
               }
           }
       }
    }

     

         2) Clean Architecture는 Use Cases(비즈니스 로직)는 Repository 인터페이스를 통해 데이터에 접근하며, 구체적인 구현(데이터베이스, API 등)은 하위 계층에 있음. 이로 인해 상위 계층이 하위 계층의 구현 세부사항에 전혀 의존하지 않는다

          이로 인해 비즈니스 로직은 외부의 변화(예: 데이터베이스, UI 변경 등)로부터 영향을 받지 않습니다.

         Use Case는 Repository 구현의 변경에 영향을 받지 않습니다. 따라서, 예를 들어 API가 변경되더라도 Repository의 구현만 수정하면 되며, 비즈니스 로직(Use Case)은 그대로 유지됨

         

    * 위의 예시를  Clean Architecture로 구현하여 의존성역전원칙이 위배되지 않는다는걸 이해해보자

     

     

    에러 처리에 있어서도, 상위 모듈은 하위 모듈의 구체적인 구현에 의존하지 않도록 해야 합니다. 이를 위해 클린 아키텍처에서는 에러 처리도 추상화하는 것이 일반적입니다.

     

    enum NetworkError: Error {
        case timeout
        case noConnection
        case invalidData
    }
    
    enum TransferLimitError: Error {
        case networkError(NetworkError)
        case serverError
        case unknownError
    }

     

    이제 Use Case에서는 ACService를 사용하여 데이터를 가져오고, 서비스 레이어의 에러를 TransferLimitError로 변환합니다.

    class FetchTransferLimitUseCase {
        private let acService: ACService
    
        init(acService: ACService) {
            self.acService = acService
        }
    
        func execute(IACN: String, completion: @escaping(Result<AC4192Q1OutRec, TransferLimitError>) -> Void) {
            acService.sendAC4192Q1(IACN: IACN) { result in
                switch result {
                case .success(let output):
                    completion(.success(output))
                case .failure(let networkError):
                    switch networkError {
                    case .timeout:
                        completion(.failure(.networkError(.timeout)))
                    case .noConnection:
                        completion(.failure(.networkError(.noConnection)))
                    case .invalidData:
                        completion(.failure(.networkError(.invalidData)))
                    }
                }
            }
        }
    }

     

    이제 ViewModel에서는 더 추상화된 에러만을 받게 됩니다.

    class TransferViewModel {
        private let fetchTransferLimitUseCase: FetchTransferLimitUseCase
    
        init(fetchTransferLimitUseCase: FetchTransferLimitUseCase) {
            self.fetchTransferLimitUseCase = fetchTransferLimitUseCase
        }
    
        func loadTransferLimitAmount() {
            fetchTransferLimitUseCase.execute(IACN: "someAccount") { result in
                switch result {
                case .success(let output):
                    UILabel().text = output.TM1_TRNF_LMT_AMT
                    UILabel().text = output.DY1_SAMT_TRNF_LMT_AMT
                case .failure(let error):
                    // 이제 ViewModel은 TransferLimitError에만 의존함
                    switch error {
                    case .networkError(let networkError):
                        // NetworkError에 따라 처리
                        switch networkError {
                        case .timeout:
                            print("Request timed out.")
                        case .noConnection:
                            print("No connection available.")
                        case .invalidData:
                            print("Received invalid data.")
                        }
                    case .serverError:
                        print("Server error occurred.")
                    case .unknownError:
                        print("Unknown error occurred.")
                    }
                }
            }
        }
    }

     

    이 예제에서 FetchTransferLimitUseCase는 ACService에서 발생한 구체적인 NetworkError를 받아 이를 TransferLimitError로 변환하여 ViewModel에 전달합니다. ViewModel은 이제 구체적인 서비스 레이어의 에러 처리와는 독립적으로 동작하며, Use Case가 제공하는 추상화된 에러에만 의존하게 됩니다. 이렇게 하면 서비스 레이어의 구현이 변경되더라도, ViewModel의 수정이 최소화됩니다.

     

     

    UseCase를 사용하는 지점에서는 해당 Validation 자체에는 관여하지 않고, Presentation에 필요한 정보만 받아 처리할 수 있게 되었습니다. ViewModel이 보다 간결해졌습니다! 😊

     

     

     

     

    [ UseCase사용 장점]

    🚫 코드 중복을 방지

    핵심 비즈니스 로직을 캡슐화하고, 재사용 할 수 있도록 함으로써 같은 코드가 여러 곳에 산재하지 않도록 합니다.

    🐜 책임을 각자 나눠 갖기 때문에, 커다란 객체를 피할 수 있음!

    • 자칫하면 모든 역할을 Presentation Layer에 맡겨서, 아주 비대한 비즈니스 객체(ViewModel, Reactor, Interactor..)를 만들어낼 수도 있습니다. UseCase에 책임을 한가지씩 위임해서 이 문제를 해결할 수 있습니다!
    • 외부 세계 (서버, DB 등)에 의존하지 못하도록 강제되기 때문에 자동으로 작아지는 Presentation 객체!

    https://medium.com/prnd/%EF%B8%8F-prnd-ios%ED%8C%80%EC%9D%98-usecase-%ED%99%9C%EC%9A%A9%EA%B8%B0-e4ddbef274a1

     


     

         

         

         2. 모듈화와 재사용성

         - MVVM은 "기능별"로 모듈을 나눔.

         ex) 주문,이체 각 화면별로 MVVM모듈을 구성하므로 하나의 비즈니스 로직이 여러 ViewModel에서 재사용되어 여러 곳에 중복될 가능성이 있다.

         ex) 이체 관련 로직은 주문화면, RP매수화면 등 다양한 화면에서 사용하는데, 이때마다 Service에 구현해주면 결국 동일한 로직이 화면별 ViewModel에 의존하여 중복됨.

         

         - Clean Architecture의 UseCase 모듈에 이체관련 로직을 독립적으로 분리시키므로 모든 화면에서 재사용이 가능함.

         

         

         

     

    댓글

Designed by Tistory.