의존성
- 어떤 객체가 예정된 작업을 정상적으로 수행하기 위해 다른 객체를 필요로 하는 경우 두 객체 사이에 의존성이 존재한다고 말한다.
- 의존성을 다른 말로하면 의존되는 요소가 변경될 때 의존하는 요소도 함께 변경될 수 있다는 것을 의미한다. 따라서 의존성은 변경에 의한 영향의 전파 가능성을 암시한다.
- 의존성은 방향성을 가지며 항상 단방향이다.
- 의존성 예시
- 객체 내부 메서드 수행시 다른 객체를 인자로 넘겨받아 사용할 때
- 인스턴스 변수로 다른 객체를 가지고 있을 때
- 인터페이스를 구현할 때, 인터페이스 타입과 오퍼레이션들에 의존
- 부모 클래스를 상속받을 때
- 의존성 전이 : 자신이 어떤 객체에 의존하고 있다면, 그 의존하는 객체가 가지고 있는 또 다른 객체와의 의존성까지 전이된다.
- 런타임 의존성 = 객체 사이 의존성
- 컴파일 의존성 = 클래스 사이 의존성
의존성과 결합도
- 의존성은 협력을 위해 반드시 필요하다. 하지만
바람직한 의존성
으로 존재해야 한다.
- 바람직한 의존성은
재사용
과 관련있다.
- 어떤 의존성이 다양한 환경에서 클래스를 재사용할 수 있다면 그 의존성은 바람직하다고 할 수 있다.
- 이를 가능하게 하는 유일한 방법은, 타입 추상화를 통한 느슨한 결합도를 만드는 것이다.
- 결합도를 느슨하게 만들기 위해서는 협력하는 대상에 대해 필요한 정보 외에는 최대한 감추는 것이 중요하다.
명시적 의존성
명시적 의존성(explicit dependency)
: 의존 대상을 생성자의 인자로 전달하여 의존성을 명시적으로 퍼블릭 인터페이스에 노출하는것
숨겨진 의존성(hidden dependency)
: 생성자 내부에서 구체클래스를 생성해서 의존성을 연결하는 방식은 의존성을 외부로부터 감추는 것
- 의존성이 명시적이지 않으면 의존성을 파악하기 위해 내부 구현을 직접 살펴봐야 한다.
- 더 큰 문제는 클래슬 다른 컨텍스트에서 재사용하기 위해 내부 구현을 직접 변경해야 한다는 것이다.
- 바람직한 의존성을 명시적으로 드러내는 것이 유연하고 재사용 가능한 설계로 이어지게 해준다.
new 연산자
- new 연산자를 사용한다는 것은 직접 사용할 클래스 이름을 확정하는 것을 말한다.
- 이는 구체 클래스에 직접 의존하게 하고 결과적으로 결합도가 높아지게 된다.
- 해결방법은
인스턴스를 생성하는 로직
과 생성된 인스턴스를 사용하는 로직
을 분리하는 것이다.
생성 사용 분리
- 객체 생성을 피할 수 는 없다. 어딘가에서는 반드시 객체를 생성해야 한다.
- 유연하고 재사용 가능한 설계를 원한다면 객체와 관련된 두 가지 책임을 서로 다른 객체로 분리해야 한다.
- 하나는 객체를 생성하는 것이고, 다른 하나는 객체를 사용하는 것이다.
- 가장 보편적인 방법은 객체를 생성할 책임을 클라이언트로 옮기는 것이다.
- 현재의 컨텍스트에 관한 결정권을 가지고 있는 클라이언트로 컨텍스트에 대한 지식을 옮김으로써 특정한 클라이언트에 결합되지 않고 독립적일 수 있다.
FACTORY 추가하기
- 객체 생성과 관련된 책임만 전담하는 별도의 객체를 추가하고 client는 이 객체를 사용하도록 만들 수 있다.
- 이처럼 생성과 사용을 분리하기 위해 객체 생성에 특화된 객체를 FACTORY라고 부른다.
- 모든 책임을 도메인 객체에게 할당하면
낮은 응집도
, 높은 결합도
, 재사용성 저하
와 같은 심각한 문제점에 봉착하게 될 가능성이 높아진다.
- 이 경우 도메인 개념을 표현한 객체가 아닌 설계자가 편의를 위해 임의로 만들어낸 가공의 객체에게 책임을 할당해서 문제를 해결해야 한다. 이를
PURE FABRICATION
이라고 부른다.
의존성 주입
- 생성과 사용을 분리하면 사용하는 객체는 어디선가 생성된 인스턴스를 전달받아서 사용해야한다.
- 외부의 독립적인 객체가 인스턴스를 생성한 후 이를 전달해서 의존성을 해결하는 방법을 의존성 주입이라고 부른다.
- 생성자 주입(constructor injection)
- 객체를 생성하는 시점에 생성자를 통한 의존성 해결
- setter 주입(setter injection)
- 객체 생성 후 setter 메서드를 통한 의존성 해결
- 메서드 주입(method injection)
사용과 생성의 책임을 분리하고, 의존성을 생성자에 명시적으로 드러내고, 구체 클래스가 아닌 추상 클래스에 의존하게 함으로써 설계를 유연하게 만들 수 있다.
의존성 역전 원칙(DIP)
- 핵심은 추상화에 의존하는 것이다.
- 고수준 모듈은 저수준 모듈의 구현에 의존하면 안된다. 저수준 모듈이 고수준 모듈에서 정의한 추상 타입에 의존해야 한다.
- 즉, 두 모듈 모두 추상화에 의존해야 한다.
고수준 모듈과 저수준 모듈
- 고수준 모듈: 의미있는 단일 기능을 제공
- 저수준 모듈: 고수준 모듈의 기능을 구현하기 위해 필요한 하위 기능의 실제 구현
고수준 관점에서 추상화
- 고수준 입장에서 저수준 모듈을 추상화 할 때, 구현 입장에서 추상화하지 말자.
DIP는 좋은 설계 가능성을 높인다.
- 고수준 모듈의 변경을 최소화하면서 저수준 모듈의 변경 유연함을 높임
- 처음부터 바로 좋은 설꼐가 나오지는 않음
- 요구사항/업무 이해가 높이지면서 저수준 모듈을 인지하고 상위 수준에서 저수준 모듈을 추상화 시도
결론
유연한 설계의 핵심은 의존성을 관리하는 것이다.
사용과 생성의 책임을 명확히 분리하고(낮은 결합도), 의존성을 생성자에 명시적으로 드러내고(의존성 주입), 구체 클래스가 아닌 추상 클래스에 의존하게 함(의존성 역전 원칙)으로써 설계를 유연하게 만들 수 있다.
이때 객체를 생성할 책임을 담당할 객체나 객체 생성 메커니즘을 결정하는 시점은 책임 할당의 마지막 단계로 미뤄야만 한다.
참고자료
- NHN 기술세미나, 객체지향 입문 - 최범균
- 오브젝트 - 조영호