의존성 관리
- 잘 설계된 객체들은 작고 응집도 높은 객체들의 협력으로 구성된다.
- 책임이 명확하고 한 가지 일만 하는 객체를 의미한다.
- 한 가지 일만 하는 객체들은 단독으로 사용할 수 없다. 협력이 필요하다.
- 과도한 협력은 서로를 너무 잘 알게 되는 역효과가 있다.
- 너무 많이 상대에 대해서 알게 된다면 서로에게 너무 의존해서 서로가 서로의 발전을 방해하는 존재가 될 수도 있다.
변경, 의존성
- 협력을 하기 위해서 다른 객체를 필요로 할 떄 두 객체 사이에 의존성이 생긴다.
- 실행 시점, 구현 시점에서 협력은 서로 다른 의미를 가진다.
- 실행 시점 : 의존하는 객체가 정상적으로 동작하기 위해서는 실행 시에 의존 대상 객체가 반드시 있어야 한다.
- 구현 시점 : 의존 대상 객체가 변경될 경우 의존하는 객체도 함께 변경된다.
- 객체가 예정된 작업을 정상적으로 수행하기 위해서 다른 객체를 필요로 하는 경우 두 객체 사이의 의존성이 존재한다고 한다.
- 의존성은 방향성을 가지며 항상 단방향이다.
- 이는 의존하고 있는 대상을 변경하면 그에 따른 사이드 이펙트가 사용하는 곳에도 변경을 만든다는 의미가 된다.
의존성 전이
- 의존성 전이(transitive dependency)는 A가 B를 의존하고 B가 C를 의존하면 결과적으로 A가 C를 간접적으로 의존하게 된다는 것을 의미한다.
- 실제 의존성 전이 문제는 캡슐화 여부에와 정도에 따라 달라진다.
- 의존성 종류를 1. 직접 의존성(directDependency), 2. 간접 의존성(indirectDependency)로 나누기도 한다.
- 직접 의존성은 한 요소가 다른 요소에 직접 의존하는 경우
- 간접 의존성이란 직접적인 관계는 존재하지 않지만 의존성 전이에 의해 영향이 전파되는 경우를 의미한다.
- 의존성은 의존하고 있는 대상의 변경에 영향을 받을 수 있는 가능성이라고 할 수 있다.
런타임, 컴파일 타임 의존성
런타임(runTime)
- 애플리케이션이 실행되는 시점에 생기는 의존성
- 객체 사이의 의존성을 의미한다.
컴파일(compile)
- 작성된 코드를 컴파일하는 시점(, 문맥에 따라서는 코드 그 자체)에 생기는 의존성
- 코드 관점에서 클래스 간의 의존성을 의미한다.
유연하고 재사용 가능한 설계를 창조하기 위해서는 동일한 소스코드 구조를 가지고 다양한 실행 구조를 만들 수 있어야 한다. 어떤 클래스의 인스턴스가 다양한 클래스의 인스턴스와 협력하기 위해서는 협력할 인스턴스의 구체적인 클래스를 알아서는 안 된다. 실제로 협력한 객체가 어떤 것인지는 런타임에 해결해야 한다.
컨텍스트 독립성
- 구체적 클래스를 알면 클래스가 사용되는 특정한 문맥에 강하게 결합된다.
- 클래스가 특정한 문맥에 강하게 결합되면 다른 문맥에서는 사용하기 어려워진다.
- 문맥에 대해 최소한의 가정만으로 이뤄져 있다면 다른 문맥에서 재사용하기가 더 수월해진다. 이를 컨텍스트 독립성이라고 부른다.
의존성 해결
- 컴파일 타임 의존성은 런타임 의존성으로 대체되는 것이 좋다.
- 컴파일 타임 의존성을 실행 컨텐스트에 맞는 적절한 런타임 의존성으로 교체하는 것을 의존성 해결이라고 한다.
- 굳이 따져서 단점을 보면 객체 생성 이후에 협력에 필요한 의존 대상을 설정하기에 객체 생성, 의존 대상 설정까지 불완전할 수 있다는 점이 있다.
의존성, 결합도
- 객체지향의 기본은 협력이다.
- 객체들이 협력하기 위해서는 서로의 존재, 수행 가능한 책임을 알아야 한다. 이는 의존성이 된다.
- 의존성 자체보다 중요한 것은 “어느 정도 의존하는가?”다.
- 목표는 올바르게 의존하는 것이다. 의존성은 재사용성과 관련이 있다.
- 다양한 환경에서 재사용할 수 없다면 바람직하지 못한 의존성이 된다.
- 컨텍스트에 독립적이어야 한다. 컨텍스트에 강결합되면 바람직하지 않다.
- 바람직한 의존성은 어려 환경에서 다양하게 재사용될 수 있는 가능성을 열어놓은 의존성을 의미한다.
- 결합도가 위 개념을 표현하는 좋은 단어다.
부모-자식 간 결합도
- 상속은 서로 간 강결합을 만든다.
- 많이 알수록 결합이 강해지는데, 이는 변경에 취약하고 재사용을 어렵게 한다.
추상화
- 양상, 세부 사항, 구조를 더 명확하게 이해하기 위해서 의도적으로 살을 생략하고 뼈를 남겨서 복잡도를 극복하는 방법이다.
- 구체적인 대상이 아닌 추상적 대상에 의존하면 결합도는 낮아진다.
- 런타임에 살은 결정된다.
결합도 정도 (1: 상대적 강결합 3: 상대적 약결합)
- 구체 클래스 의존성
- 추상 클래스 의존성
- 인터페이스 의존성
- 인터페이스 의존은 상속 계층을 모르더라도 협력을 할 수 있게 한다.
- 의존 대상이 추상적이면 추상적일수록 결합도는 낮아진다.
명시적 의존성
- 의존 대상을 ‘생성자 인자로 전달 받는 방법’, ‘생성자 안에서 직접 생성하는 방법’, ‘setter’와 같이 초기화 하는 방법에서도 명시적으로 퍼블릭 인터페이스에 노출시키는 것을 명시적 의존성이라고 한다.
- 의존성이 외부로 들어나지 않은 경우를 숨겨진 의존성(hidden dependency)라고 부른다.
- 의존성이 명시적이지 낳으면 내부 구현을 살펴야 한다.
- 의존성을 명시적으로 드러내야 다른 컨텍스트에서 재사용할 수 있다.
//이것 보단
public class Example {
private DependencyExample dependencyExample;
private Integer a;
public Example (Integer a, Double b){
this.a = a;
dependencyExample = new DependencyExample(b);
}
}
//이게 낫다.
public class Example {
private AbstractDependencyExample abstractDpendencyExample;
private Integer a;
public Example (Integer a, AbstractDependencyExample abstractDpendencyExample){
this.a = a;
this.abstractDpendencyExample = abstractDpendencyExample;
}
}
new는 피하자
- new를 사용하면 구체 클래스 이름을 기술해야 한다.
- 어떤 인자를 이용해서 클래스의 생성자를 호출해야 하는지도 알려진다.
- 이는 협력자에게 너무 많은 지식을 알도록 강요한다
- 어떤 인자들이 필요하고 그 인자들을 어떤 순서로 사용하는지에 대한 것도 알도록 하고 인자로 사용하는 구체 클래스에 대한 의존성도 추가한다.
- 사용과 생성의 책임을 분리하고, 의존성을 생성자에 명시적으로 드러내고, 구체 클래스가 아닌 클래스에 의존하게 함으로써 설계를 유연하게 한다.
물론 new가 나쁜 것만은 아니다.
- 물론 나쁜 것만은 아니다 사용할 필요가 생기면 써야한다.
- 기본 생성자를 추가하고 내부에서 다른 생성자로 체이닝 하는 방법도 있다.
public class Example {
private DependencyExample dependencyExample;
private Integer a;
public Example (Integer a, Double b){
this(a, new DependencyExample(b));
}
public Example (Integer a, DependencyExample dependencyExample){
this.a = a;
this.dependencyExample = dependencyExample;
}
}
- 트레이드 오프 대사용은 결합도와 사용성이다.
표준 클래스는 무방하다.
- java에 포함된 더 이상 변경될리 없는 클래스들은 상관 없다.
컨텍스트 확장
- A에서 사용하던 객체를 B에서도 유연하게 사용하게 하는 방법은 A에 특수한 케이스를 B에서는 미리 예외처리를 할 수 있도록 두는 방법이 있다.
- 물론 이는 오히려 훗날 일을 복잡하게 할 수도 있으나, 미리 작업을 해두면 목표하던 바는 이룰 수 있다.
조합 가능한 행동
- 어떤 객체와 협력하느냐에 따라서 객체의 행동이 달라지는 것은 유연하고 재사용 가능한 설계가 가진 특징이다.
- 이를 통해서 응집도 높은 책임을 가진 작은 객체들을 다양한 방식으로 연결해서 기능을 확장할 수 있다.
- 객체들 조합을 통해서 What을 하는지를 표현하는 식으로 구성된다.