이 글은 오브젝트라는 책을 읽고 정리한 글입니다.
8장에서는 의존성의 개념을 자세히 설명하고 결합도를 느슨하게 유지할 수 있는 다양한 설계 방법들을 설명합니다.
잘 설계된 객체지향 애플리케이션은 작고 응집도 높은 객체들로 구성된다. 작고 응집도 높은 객체란 책임의 초점이 명확하고 한 가지 일만 잘하는 객체를 의미한다.
협력을 위해서는 의존성이 필요하지만 과도한 의존성은 애플리케이션을 수정하기 어렵게 만든다. 객체지향 설계의 핵심은 협력을 위해 필요한 의존성은 유지하면서도 변경을 방해하는 의존성은 제거하는데 있다. 이런 관점에서 객체지향 설계란 의존성을 관리하는 것이고, 객체가 변화를 받아들일 수 있게 의존성을 정리하는 기술이라고 할 수 있습니다.
의존성 이해하기
변경과 의존성
어떤 객체가 협력하기 위해 다른 객체를 필요로 할 때 두 객체 사이에 의존성이 존재하게 된다. 의존성은 실행 시점과 구현 시점에 서로 다른 의미를 가진다.
- 실행 시점: 의존하는 객체가 정상적으로 동작하기 위해서는 실행 시점에 의존 대상 객체가 반드시 존재해야 한다.
- 구현 시점: 의존 대상 객체가 변경될 경우 의존하는 객체도 함께 변경된다.
public class PeriodCondition implements DiscountCondition {
private DayOfWeek dayOfWeek;
private LocalTime startTime;
private LocalTime endTime;
...
public boolean isSatisfiedBy(Screening screening) {
return screening.getStartTime().getDayOfWeek().equals(dayOfWeek) &&
startTime.compareTo(screening.getStartTime().toLocalTime()) <= 0 &&
emdTime.compareTo(screening.getStartTime().toLocalTime()) >= 0;
}
}
의존성은 방향성을 가지며 항상 단방향이다.
두 요소 사이의 의존성은 의존되는 요소가 변경될 때 의존하는 요소도 함께 변경될 수 있다. 의존성은 변경에 의한 영향의 전파 가능성을 암시한다.
의존성 전이
의존성은 함께 변경될 수 있는 가능성을 의미할 뿐 무조건적으로 의존성이 전이되는 것은 아닙니다. 의존성이 실제로 전이될지 여부는 변경의 방향과 캡슐화의 정도에 따라 달라집니다.
의존성은 전이될 수 있기 때문에 의존성의 종류를 직접 의존성과 간접 의존성으로 나누기도 합니다.
- 직접 의존성: 말 그대로 한 요소가 다른 요소에 직접 의존하는 경우
- 간접 의존성: 직접적인 관계는 존재하지 않지만 의존성 전이에 의해 영향이 전파되는 경우
의존성이란 의존하고 있는 대상의 변경에 영향을 받을 수 있는 가능성이다.
런타임 의존성과 컴파일타임 의존성
- 런타임: 애플리케이션이 실행되는 시점
- 컴파일타임: 작성된 코드를 컴파일하는 시점 / 코드 그 자체
객체지향 애플리케이션에서 런타임 시점의 주인공은 객체다. 반면 컴파일타임 시점의 주인공은 클래스입니다.
- 런타임 의존성: 객체 사이의 의존성
- 컴파일타임 의존성: 클래스 사이의 의존성
여기서 중요한 것은 런타임 시점의 의존성과 컴파일타임 시점의 의존성은 다를 수 있으며, 유연하고 재사용 가능한 코드를 설계하기 위해서는 두 종류의 의존성을 다르게 해야합니다.
어떤 클래스의 인스턴스가 다양한 클래스의 인스턴스와 협력하기 위해서는 협력할 인스턴스의 구체적인 클래스를 알아서는 안된다. 컴파일타임 구조와 런타임 구조 사이의 거리가 멀수록 설계가 유연해지고 재사용 가능해집니다.
컨텍스트 독립성
클래스는 자신과 협력할 객체의 구체적인 클래스에 대해 알아서는 안된다. 구체적인 클래스를 알면 알수록 그 클래스가 사용되는 특정한 문맥에 강하게 결합되기 때문이다.
클래스가 사용될 특정한 문맥에 대해 최소한의 가정만으로 이뤄져 있다면 다른 문맥에서 재사용하기가 더 수월해진다. 이를 컨텍스트 독립성이라고 부른다.
설계가 유연해지기 위해서는 가능한 자신이 실행될 컨텍스트에 대한 구체적인 정보를 최대한 적게 알아야 한다. 컨텍스트에 대한 정보가 적으면 적을수록 더 다양한 컨텍스트에서 재사용될 수 있기 때문이다. 결과적으로 설계는 더 유연해지고 변경에 탄력적으로 대응할 수 있게 될 것이다.
의존성 해결하기
컴파일타임 의존성은 구체적인 런타인 의존성으로 대체돼야 한다. 컴파일 의존성을 실행 컨텍스트에 맞는 적절한 런타임 의존성으로 교체하는 것을 의존성 해결이라고 부른다.
의존성을 해결하기 위해서는 일반적으로 세 가지 방법을 사용한다.
- 객체를 생성하는 시점에 생성자를 통해 의존성 해결
Movie avatar = new Movie("avatar",
Duration.ofMinutes(120),
Money.wons(10000),
new AmountDiscountPolicy());
- 객체 생성 후 setter 메서드를 통해 의존성 해결
Movie movie = new Movie(..);
movie.setDiscountPolicy(new AmountDiscountPolicy(..));
- 메서드 실행 시 인자를 이용해 의존성 해결
public class Moive{
public Money calculateMovieFee(Screening screening, DiscountPolicy discountPolicy){
return fee.minus(discountPolicy.calculateDiscountAmount(screening));
}
}
유연한 설계
의존성과 결합도
다른 환경에서 재사용하기 위해 내부 구현을 변경하게 만드는 모든 의존성은 바람직하지 않은 의존성이다. 이를 다른 용어로 결합도로 일컫는다. 어떤 두 요소 사이에 존재하는 의존성이 바람직할 때 느슨한 결합도 또는 약한 결합도를 가진다고 말한다. 반대로 두 요소 사이의 의존성이 바람직하지 못할 때 단단한 결합도 또는 강한 결합도를 가진다고 말한다.
바람직한 의존성이란 설계를 재사용하기 쉽게 만드는 의존성이다. 바람직하지 못한 의존성이란 설계를 재사용하기 어렵게 만드는 의존성이다.
지식이 결합을 낳는다
결합도의 정도는 한 요소가 자신이 의존하고 있는 다른 요소에 대해 알고 있는 정보의 양으로 결정된다. 한 요소가 다른 요소에 대해 더 많은 정보를 알고 있을수록 두 요소는 강하게 결합된다. 서로에 대해 알고 있는 지식의 양이 결합도를 결정한다.
더 많이 알수록 더 많이 결합된다. 결합도를 느슨하게 만들기 위해서는 협력하는 대상에 대해 필요한 정보 외에는 최대한 감추는 것이 중요하다. 그것이 바로 추상화다.
추상화에 의존하라
추상화란 어떤 양상, 세부사항, 구조를 좀 더 명확하게 이해하기 위해 특정 절차나 물체를 의도적으로 생략하거나 감춤으로써 복잡도를 극복하는 방법이다. 추상화를 사용하면 불필요한 정보를 감출 수 있다. 따라서 대상에 대해 알아야 하는 지식의 양을 줄일 수 있기 때문에 결합도를 느슨하게 유지할 수 있다.
일반적으로 추상화와 결합도의 관점에서 의존 대상을 다음과 같이 구분하는 것이 유용하다. 목록에서 아래쪽으로 갈수록 클라이언트가 알아야 하는 지식의 양이 적어지기 때문에 결합도가 느슨해진다.
- 구체 클래스 의존성
- 추상 클래스 의존성
- 인터페이스 의존성
명시적인 의존성
의존성의 대상을 생성자의 인자로 전달 받는 방식과 생성자 안에서 직접 생성하는 방식의 가장 큰 차이점은 퍼블릭 인터페이스를 통해 설정을 유동적으로 할 수 있는지 방법을 제공하는지 여부다.
- 명시적인 의존성: 의존성이 명시적으로 퍼블릭 인터페이스에 노출되는 경우
- 숨겨진 의존성: 의존성이 퍼블릭 인터페이스에 표현되지 않는 경우
유연하고 재사용 가능한 설계란 퍼블릭 인터페이스를 통해 의존성이 명시적으로 드러나는 설계다. 명시적인 의존성을 사용해야만 퍼블릭 인터페이스를 통해 컴파일타임 의존성을 적절한 런타임 의존성으로 교체할 수 있다.
new는 해롭다
new를 잘못 사용하면 클래스 사이의 결합도가 극단적으로 높아진다.
결합도 측면에서 new가 해로운 이유는 크게 두 가지다.
- new 연산자를 사용하기 위해서는 구체 클래스의 이름을 직접 기술해야 한다. 따라서 new를 사용하는 클라이언트는 추상화가 아닌 구체 클래스에 의존할 수밖에 없기 때문에 결합도가 높아진다.
- new 연산자는 생성하려는 구체 클래스뿐만 아니라 어떤 인자를 이용해 클래스의 생성자를 호출해야 하는지도 알아야 한다. 따라서 new를 사용하면 클라이언트가 알아야 하는 지식의 양이 늘어나기 때문에 결합도가 높아진다.
해결 방법은 인스턴스를 생성하는 로직과 생성된 인스턴스를 사용하는 로직을 분리하는 것이다.
표준 클래스에 대한 의존은 해롭지 않다
의존성이 불편한 이유는 변경에 대한 영향을 암시하기 때문이다. 따라서 변경될 확률이 거의 없는 클래스라면 의존성이 문제가 되지 않는다
조합 가능한 행동
어떤 객체와 협력하느냐에 따라 객체의 행동이 달라지는 것은 유연하고 재사용 가능한 설계가 가진 특징입니다. 유연하고 재사용 가능한 설계는 응집도 높은 책임들을 가진 작은 객체들을 다양한 방식으로 연결함으로써 애플리케이션의 기능을 쉽게 확장할 수 있습니다.
유연하고 재사용 가능한 설계는 객체가 어떻게 하는지를 나열하지 않고 객체들의 조합을 통해 무엇을 하는지를 표현하는 클래스들로 구성된다. 따라서 클래스의 인스턴스를 생성하는 코드를 보는 것만으로 객체가 어떤 일을 하는지를 쉽게 파악할 수 있다.
유연하고 재사용 가능한 객체는 작은 객체들의 행동을 조합으로써 새로운 행동을 이끌어낼 수 있는 설계다. 훌륭한 객체지향 설계란 객체가 어떻게 하는지를 표현하는 것이 아니라 객체들의 조합을 선언적으로 표현함으로써 객체들이 무엇을 하는지를 표현하는 설계다.
'📖 > 오브젝트' 카테고리의 다른 글
[오브젝트] 10장 상속과 코드 재사용 (0) | 2024.03.04 |
---|---|
[오브젝트] 9장 유연한 설계 (1) | 2024.02.26 |
[오브젝트] 7장 객체 분해 (0) | 2024.02.22 |
[오브젝트] 6장 메시지와 인터페이스 (0) | 2024.02.19 |
[오브젝트] 5장 책임 할당하기 (0) | 2024.02.19 |