Notice
Recent Posts
Recent Comments
Link
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
Archives
Today
Total
관리 메뉴

DailyCode

[SOLID] 의존관계 역전 법칙(DIP) 본문

디자인 패턴

[SOLID] 의존관계 역전 법칙(DIP)

JSKoder 2023. 4. 5. 21:23

의존관계 역전 법칙(Dependency Inversion Principle, ISP)은 객체 지향 프로그래밍에서 SOLID 원칙 중 하나입니다.

이 원칙은 상위 모듈과 하위 모듈 간의 의존성을 최소화하고, 추상화를 통해 서로간의 결합도를 낮추는 것을 목표로 합니다. 이를 통해 시스템의 확장성, 재사용성, 테스트 용이성 등이 향상되며, 유지보수가 용이해집니다. 이를 구현하기 위해 의존성 주입(Dependency Injection)과 같은 기법이 사용되곤 합니다.

의존성 주입은 별도의 게시글로 소개 하겠습니다.

정의

  • 시스템은 추상화에 의존하고, 구체화에 의존하면 안 된다
  • 상위 모듈은 하위 모듈의 구현에 의존하면 안 된다.
  • 하위 모듈(Class)이 상위 모듈에 정의한 추상 타입(Protocol)에 의존해야 하며 하위 모듈의 변경이 높은 수준의 모듈에 영향을 미치지 않아야 합니다.
  • DIP는 추상화에 대한 프로토콜을 정의하고 종속성 주입(DI)을 사용하여 객체에 종속성을 전달함으로써 Swift에 적용됩니다.

장점

  • 기능을 추가하거나 변경해도 다른 부분에 영향이 없습니다.
  • 재사용성 증가 및 종속성 감소

예제

protocol NetworkClient {
    func requestData(param: NetworkParameters, completion: @escaping (ApiResult<Data>) -> ())
    func downloadImage(url: String, complete: @escaping (UIImage?) -> ())
}

final class NetworkManager: NetworkClient {
    /**
     * @서버의 데이터를 요청합니다.
     * @creator : coder3306
     * @param type : 다운로드할 JSON 데이터에 매핑할 모델 설정
     * @param param : Alamofire Request 파라미터 설정
     * @Return : 성공 -> 매핑된 데이터 클로저 반환, 실패 -> 에러타입 반환
     */
    public func requestData(param: NetworkParameters, completion: @escaping (ApiResult<Data>) -> ()) {
        AF.request(param.url
                  , method: param.method
                  , parameters: param.parameter
                  , headers: param.header).responseData { response in
            
            switch response.result {
            case let .success(data):
                completion(.success(data))
            case let .failure(error):
                print("Alamofire Error --------------------> \(error)")
                completion(.failure(ApiError.statusCodeError))
            }
        }
    }
    
    /**
     * @이미지 데이터를 요청합니다.
     * @creator : coder3306
     * @param url : 이미지 다운로드 주소
     * @Return : 다운로드된 이미지 반환
     */
    public func downloadImage(url: String, complete: @escaping (UIImage?) -> ()) {
        AF.request(url, method: .get, parameters: nil, interceptor: nil).validate().responseData { response in
            switch response.result {
                case let .success(data):
                    if let image = UIImage(data: data) {
                        complete(image)
                        return
                    }
                    complete(nil)
                case let .failure(error):
                    complete(nil)
                    print(error)
            }
        }
    }
}

////////////////////////////////////////////
//// MARK: - Business Logic
////////////////////////////////////////////
protocol MarvelCharactersTaskInput {
    /**
     * @캐릭터 리스트 요청
     * @creator : coder3306
     * @param type : 모델 데이터
     * @param limit : 한번에 요청할 데이터 갯수
     */
    func requestCharactersList<T: Decodable>(type: T.Type, for limit: Int)
}

protocol MarvelCharactersTaskOutput: AnyObject {
    /**
     * @캐릭터 리스트 응답 데이터
     * @creator : coder3306
     * @param characters : 리스트 모델 데이터
     */
    func responseCharactersList(_ characters: Marvel?)
}

final class MarvelCharactersTask: MarvelCharactersTaskInput {
    /// 응답 데이터 델리게이트 프로토콜 설정
    weak var output: MarvelCharactersTaskOutput?
    /// 요청 url 주소
    private let url: MarvelURL
    /// 네트워크 설정
    private let networkClient: NetworkClient
    
    /**
     * @비즈니스 로직 초기화
     * @creator : coder3306
     * @param networkClient : 네트워크 설정 프로토콜
     * @param url : 요청 URL 주소
     */
    init(networkClient: NetworkClient, url: MarvelURL) {
        self.networkClient = networkClient
        self.url = url
    }
    
	/**
     * @캐릭터 리스트 요청
     * @creator : coder3306
     * @param type : 모델 데이터
     * @param limit : 한번에 호출할 데이터 갯수 설정
     * @Return : Output으로 매핑 처리된 데이터 전달
     */
    func requestCharactersList<T: Decodable>(type: T.Type, for limit: Int) {
        var parameter = NetworkParameters(url: url.resource, method: .get)
        guard let requestMarvelInfo = parameter.getMarvelData(limit) else { return }

        networkClient.requestData(param: requestMarvelInfo) { [weak self] result in
            switch result {
                case .success(let data):
                    do {
                        let parsing = try JSONDecoder().decode(type, from: data)
                        self?.output?.responseCharactersList(parsing as? Marvel)
                    } catch {
                        self?.output?.responseCharactersList(nil)
                        print("JSON Decoding error ------------ >>> \(error)")
                    }
                case .failure(let apiError):
                    self?.output?.responseCharactersList(nil)
                    print("Api Call error ------------ >>> \(apiError)")
            }
        }
    }
}

코드의 주요 동작 흐름은 다음과 같습니다.

1. NetworkClient 프로토콜: NetworkManager와 같은 하위 수준의 네트워크 클래스를 추상화하는 프로토콜입니다. 이를 통해 상위 수준 모듈이 하위 수준 모듈에 직접 의존하지 않고 프로토콜을 통해 의존하게 됩니다. 이는 DIP 원칙을 따르는 예시입니다.
2. MarvelCharactersTask 클래스의 init() 메서드: 이 클래스는 초기화 시점에 NetworkClient 프로토콜을 구현한 인스턴스를 주입받습니다. 여기서는 NetworkManager 클래스의 인스턴스를 주입하고 있습니다. 이렇게 의존성을 외부에서 주입하는 것이 DI 입니다.

목표

이 디자인 패턴의 목표는 소프트웨어 아키텍처의 유연성과 안정성을 향상시키기 위해 결합도를 낮추는 것입니다. 이를 통해 각 구성 요소가 독립적으로 변경 가능하고, 새로운 요구 사항에 쉽게 적응할 수 있게 됩니다.

 

추가적인 예제가 궁금하시다면 깃허브에서 확인해주세요.

https://github.com/coder3306/MVC-Marvel