도니의 iOS 프로그래밍 세상

[오브젝트 2회독] 8장 - 의존성 관리하기 본문

OOP

[오브젝트 2회독] 8장 - 의존성 관리하기

Donee 2024. 11. 19. 19:30
  • 잘 설계된 객체 지향 App은 작고 응집도 높은 객체로 구성
  • 작고 응집도가 높기 때문에 다른 객체와의 협력이 필수적이고 이는 의존성이 필연적으로 수반됨
  • 과도한 의존성은 App의 수정을 어렵게 하기에 이를 관리해야 함

1. 의존성 이해하기

두가지 의존성

  • 실행 시점 의존성: 객체 정상 동작을 위해선 실행 시 의존 대상 객체가 반드시 존재해야 함
  • 구현 시점: 의존 대상 객체 변경시, 의존하는 개체또한 변경

런타임 의존성과 컴파일 타임 의존성

  • 런타임 의존성
    • 애플리케이션 실행 시점
  • 컴파일 타임 의존성
    • 컴파일 시점 or 코드 그 자체를 의미하며 이는 코드의 구조를 의미함

런타임의 객체 사이의 의존성을 의미하고, 컴파일 타임때는 클래스간 의존성을 의미함

객체가 협력하기 위해선 구체적인 클래스를 알아선 안되고, 런타임에 협력 객체가 결정되어야 함

결과적으로 컴파일 타임 의존성과 런타임 의존성이 멀수록 유연하고 재사용이 가능한 설계가 됨(but, 복잡해진다)

의존성 해결하기

  • 컴파일 타임 의존성은 런타임 의존성으로 대체되어야 하며 이를 “의존성 해결”이라고 부름
  • 하지만 결국 누군가는 “구체 클래스”의 인스턴스를 생성해야 하며, 3가지 방법을 사용함
    • 객체 생성 시점에 생성자를 통한 의존성 해결
    • let movie = Movie(category: Romance())
    • setter 메서드를 통한 해결
    • let movie = Movie() movie.setCategory(Romance())
    • 메서드 실행 인자를 이용한 해결
      • 생성자나 setter와 달리 1회성이라면 메서드때 주입 받는걸로도 충분
      let movie = Movie()
      let result = movie.action(category: Romance())
      

setter가 생성자를 통한 의존성 해결에 비해 설계를 더욱 유연하게 만드는데, 그 이유는 실행 시점에 의존성 대상을 변경할 수 있기 때문이다. (생성자를 통한 의존성은 변경할 수 없음)

하지만, set하기 전까지 객체의 상태가 불완전한 단점이 존재하기 때문에 두가지를 혼합한 방식을 사용함

let movie = Movie(category: None())
movie.setCategory(Romance())

다음과 같이 Empty 객체를 사용함으로써, 불완전한 상태의 단점을 보완하고 다양한 경로로 의존 대상을 변경하여 유연한 설계를 할 수 있음

2. 유연한 설계

의존성은 객체들의 협력을 가능하게 만들지만, 과하면 문제가 발생

이때 바람직한 의존성의 정도를 가르키는 용어인 강한 결합도, 약한 결합도

  • 강한 결합도: 바람직하지 않은 의존성
  • 약한 결합도: 바람직한 의존성

지식이 결합도를 낳는다

  • A와 B객체 사이에서 서로의 정보를 많이 알수록 결합도는 강해지며, 적을수록 약해짐
  • 결국 지식의 양이 결합도를 결정하며, 이를 위한 효과적인 방법인 추상화

추상화에 의존하라

추상화란 구조를 명확하게 이해하기 위해 특정 절차를 의도적으로 감춤으로써 복잡도를 극복하는 방법

  • 의존성의 종류
    • 구체 클래스 의존성
    • 추상 클래스 의존성
    • 인터페이스 의존성
    → 아래로 갈수록 결합도가 느슨해짐(알아야 하는 지식의 양이 감소)
  • 인터페이스 의존시 다양한 객체들의 동일한 메시지를 수신할 수 있어 간단하게 Context를 확장하는 게 가능
  • 결국 의존 대상이 추상적일수록 결합도는 더욱 낮아진다.

명시적인 의존성

의존성을 퍼블릭 인터페이스로 노출할지, 내부 구현에 노출할지에 따라 의존성의 종류가 변경된다

  • 명시적 의존성
    • 퍼블릭 인터페이스에 노출되어 외부에서 사용할 때 해당 객체의 의존성을 명확하게 파악 가능
  • 숨겨진 의존성
    • 퍼블릭 인터페이스에 표현되지 않는 의존성으로 내부 구현을 보지 않는한 파악 불가
    • ex.) A객체가 B객체에 의존할 때 다음과 같이 코드로 되어 있는 경우
    class A {
       private let b = B()
    }
    

숨겨진 의존성의 단점은 재사용을 위해 내부 구현을 직접 파악해서 수정해야 한다

결국, 의존성을 명시적으로 표현함으로써, 적절한 시기에 런타임 의존성을 바꿔 더욱 유연한 설계 가능

이때, 표준 클래스에 대한 숨겨진 의존성은 크게 해롭지 않음

  • 의존성이 불편한 이유는 변경에 대한 영향을 암시하기 때문이지만, 표준 클래스는 거의 변경되지 않기 때문이다.

결론

  • 잘 설계된 객체 지향 App에서는 객체들간 협력이 필수적이기에, 의존성이 반드시 수반됨
  • 의존성에는 객체들간 의존성을 의미하는 런타임 의존성, 클래스간 의존성을 의미하는 컴파일 타임 의존성으로 나뉨
    • 런타임 의존성과 컴파일 타임 때 의존성이 멀수록 유연한 설계(why? 객체가 다양한 객체와 협력할 수 있기 때문)
  • 컴파일 타임 의존성과 런타임 의존성을 다르게 하기 위해선 3가지 기법이 적용
    • 생성자를 통한 의존성 해결
    • setter를 통한 해결
    • 메서드를 통한 해결
  • setter를 통한 의존성 주입이 생성자를 통한 의존성 주입보다 더욱 유연한 설계 가능
    • 실행 시점에 다양하게 의존성을 변경할 수 있음
    • but, setter전에 객체가 불완전해지기에 이를 해결하고자 Empty 생성자 주입과 혼합해서 해결함
  • 객체들간 결합도는 필연적이기 때문에 의존성을 “명시적”으로 하느냐, “숨기느냐”가 중요한 척도
    • 객체의 결합을 명시적으로 Public interface로 노출함으로써, 적절한 시기에 의존성을 다양하게 처리할 수 있음
    • 객체 결합을 내부 구현을 통해 처리한다면, 추후 변경시에 내부 구현을 모두 파악해서 수정해야 하기 때문에 좋지 않음
  • 하지만 설계는 trade-off이기 때문에 런타임 의존성, 명시적 의존성이 반드시 좋다고 말할 수 없다.(복잡도가 올라가기 때문)
Comments