it 책/오브젝트: 코드로 이해하는 객체지향 설계
오브젝트: 코드로 이해하는 객체지향 설계 8장을 읽으며
lgvv
2024. 11. 14. 00:14
오브젝트: 코드로 이해하는 객체지향 설계 8장을 읽으며
느낀점
의존성에 대해서 더 잘 알게 되었음. 사내에서 이렇게 하면 더 좋겠다라고 생각해서 작성한 코드가 컨텍스트 확장과 조합 가능한 행동을 따른다는 데서 짜릿함을 느낌
의존성 관리하기
잘 설계된 애플리케이션은 작고 응집도 높은 객체들로 구성
- 협력은 필수적이지만 과도한 협력은 설계를 곤경에 빠뜨리기도 함.
- 객체지향 설계란 의존성을 관리하는 것이고 객체가 변화를 받아들일 수 있게 의존성을 정리하는 기술
변경과 의존성
객체가 협력하기 위해 다른 객체를 필요로 할 때 의존성이 존재. 의존성은 실행 시점과 구현 시점에 서로 다른 의미를 가짐
- 실행 시점: 의존하는 객체가 정상적으로 동작하기 위해서는 실행 시에 의존 대상 객체가 반드시 존재해야 함
- 구현 시점: 이존 대상 객체가 변경될 경우 의존하는 객체도 함께 변경
- 다른 객체를 필요로 하는 경우 두 객체 사이에 의존성이 존재한다고 말함. 의존성을 방향성을 가지며 항상 단방향이다.
의존성 전이(transitive dependency)
의존하는 대상에 대해서도 자동적으로 의존하게 된다는 것
- 의존성은 전이될 수 있기 때문에 의존성의 종류를 직접 의존성(direct dependeny)와 간접 의존성(indirect dependency)로 구분하기도 함.
- A ---> B ---> C로 되어 있다면
- A는 B에 직접 의존하며 C에는 간접 의존
- A는 B의 변경에 영향을 받으며, 그 역은 성립하지 않음.
런타임 의존성과 컴파일타임 의존성
런타임 의존성과 컴파일타임 의존성을 알아야 함.
- 런타임은 말 그대로 애플리케이션이 실행되는 시점을 가르킴
- 런타임 의존성은 객체 사이의 의존성
- 컴파일타임은 말그대로 작성된 코드가 컴파일 되는 시점
- 동적 타입의 언어의 경우에는 컴파일타임이 존재하지 않기 때문에 컴파일타임 의존성이라는 용어를 실제로 컴파일이 수행되는 시점으로 이해하면 의미가 모호해질 수 있음.
- 컴파일타임 의존성은 클래스 사이의 의존성
- 클래스가 협력할 객체의 클래스를 명시적으로 드러내고 있으면, 다른 클래스의 인스턴스와 협력할 가능성 자체가 낮아짐.
- 컴파일타임 구조와 런타임 구조 사이의 거리가 멀면 멀수록 설계가 유연해지고 재사용 가능
/*
✅ 컴파일 타임 의존성 예제
클래스를 명시적으로 드러내는 예제
BFramework로 확장해야 할 경우 재사용이 어려워짐
*/
class AFramework {}
class BFramework {}
class Object {
var frameworkA: AFramework
init(frameworkA: AFramework) {
self.frameworkA = frameworkA
}
}
/*
✅ 런타임 의존성 예제
이처럼 설계할 경우 확장이 쉽고 재사용도 쉬움
*/
protocol Frameworkable {}
class AFramework: Frameworkable {}
class BFramework: Frameworkable {}
class Object {
var framework: Frameworkable
init(framework: Frameworkable) {
self.framework = framework
}
}
/*
✅ Object의 의존 관계 살펴보기
컴파일 타임 의존성으로 설계할 경우 명시적으로 드러내어
Object -> AFramework 형태
Object -> BFramework 형태
런타임 의존성으로 설계할 경우
컴파일 타임: Object -> Frameworkable
런타임: Object -> AFramework, BFramework로 실행시점에 유연하게 사용 가능
*/
컨텍스트 독립성
클래스가 특정한 문맥에 대해 최소한의 가정만으로 이워져 있다면 다른 문맥에서 재사용하기가 더 수월해짐.
- 구체적인 클래스를 알면 알수록 특정한 문맥에 강하게 결합되기 때문
- 특정 문맥에 대해 최소한으로 안다면 재사용하기 쉬워지고 이를 컨텍스트 독립성이라고 부름
의존성 해결하기
컴파일타임 의존성은 구체적인 런타임 의존성으로 대체되어야 함.
- 컴파일타임 의존성이 실행 컨텍스트에 맞는 적절한 런타임 의존성으로 교체되는 것을 의존성 해결이라고 부름. 일반적으로 3가지 방법 사용
- 객체를 생성하는 시점에 생성자를 통해 의존성 해결
- 객체 생성 후 setter 메서드를 통해 의존성 해결
- 메서드 실행 시 인자를 이용해 의존성 해결
의존성과 결합도
설계를 유연하고 재사용 가능하게 만들기로 결정했다면 의존성을 관리하는 데 몇가지 유용한 원칙과 기법을 익혀야할 필요 있음
- 객체지향 패러다임의 근간은 협력
- 서로의 존재와 수행 가능한 책임을 알아야 함
- 모든 의존성이 나쁜 것은 아니지만, 과하면 문제가 될 수 있음.
- 문제는 의존성의 존재가 아니라 의존성의 정도
- 바람직한 의존성이란 재상용성과 관련이 있음
- 어떤 의존성이 다양한 환경에서 클래스를 재사용할 수 없도록 제한한다면 그 의존성은 바람직하지 못한 것.
- 다시 말해 컨텍스트에 독립적인 의존성은 바람직한 의존성이고 특정한 컨텍스트에 재사용할 수 있는 유일한 방법은 구현을 변경하는 것 뿐
- 다른 환경에서 재사용하기 위해 내부 구현을 변경하게 만드는 모든 의존성은 바람직하지 않은 의존성임
- 바람직한 의존성과 바람직하지 못한 의존성을 가리키는 좀 더 세련된 용어는 결합도
- 의존성이 바람직할 때 두 요소가 느슨한 결합도(loose coupling)또한 약한 결합도(weak coupling)를 가진다고 말함
- 더 많이 알수록 더 강하게 결합
추상화에 의존하라
추상화를 통해 대상에 대해 알아야하는 지식의 양을 줄일 수 있어서 결합도를 느슨하게 할 수 있음
- 일반적으로 추상화와 결합도의 관점에서 의존 대상을 다음과 같이 구분하는 것이 우용 아래쪽으로 갈수록 클라이언트가 알아야 하는 지식의 양이 적어지기 때문에 결합도가 느슨해짐
- 구체 클래스 의존성(concrete class dependency)
- 추상 클래스 의존성(abstract class dependency)
- 인터페이스 의존성(interface dependency)
- 핵심은 의존하는 대상이 더 추상적일수록 결합도는 더 낮아짐.
// ✅ 구체 클래스 의존성(concrete class dependency)
class A {}
class B {}
class Obejct {
var a: A
var b: B
}
// ✅ 추상 클래스 의존성(abstract class dependency)
class BaseObject {}
class A: BaseObject {}
class B: BaseObject {}
class Obejct {
var baseObject: BaseObject
}
// ✅ 인터페이스 의존성(interface dependency)
protocol objectable {}
class A: objectable {}
class B: objectable {}
class Obejct {
var obj: objectable
}
명시적인 의존성
생성자에 따라서 결합도가 불필요하게 높아질 수 있음
- 결합도를 낮추기 위해서는 인터페이스를 외부에 노출하기
- 내부에서 직접 생성하는건 인터페이스에 표현되지 않으며 이를 숨겨진 의존성(hidden dependency)라고 부름
- 의존성이 명시적이지 않으면 의존성을 파악하기 위해 내부 구현을 직접 살펴볼 수 밖에 없으며 고통스러운 작업
- 더 큰 문제는 의존성이 명시적이지 않으면 클래스를 다른 컨텍스트에서 재사용하기 위해 내부 구현을 직접 변경해야 함.
- 생성의 책임을 클라이언트에게 옮기고, 알려야 유연하고 재사용 가능해질 것
protocol Frameworkable {}
class AFramework: Frameworkable {}
class BFramework: Frameworkable {}
class Object {
var framework: Frameworkable
// ❌ 생성자를 내부에 명시함으로써 불필요하게 결합도가 높아짐
func configureA() {
// ❌ 내부에서 직접 생성해서 히든 디펜던시
self.framework = AFramework()
}
}
protocol Frameworkable {}
class AFramework: Frameworkable {}
class BFramework: Frameworkable {}
class Object {
var framework: Frameworkable
// ✅ 생성자를 외부에 노출시킴으로써 결함도를 낮춤
func configure(framework: Frameworkable) {
self.framework = framework
}
}
가끔은 생성해도 무방하다
클래스 안에서 객체의 인스턴스를 직접 생성하는 방법이 유용한 경우도 있음.
- 예를들어 대부분의 경우에는 A 인스턴스와 협력하고 가끔 B와 협력한다고 가정
- 여기서 트레이드 오프 대상은 결합도와 사용성임
protocol Frameworkable {}
class AFramework: Frameworkable {}
class BFramework: Frameworkable {}
struct Object {
var framework: Frameworkable
init(framework: Frameworkable = AFramework()) {
self.framework = framework
}
}
표준 클래스에 대한 의존은 해롭지 않다
의존성이 불편한 이유는 그것이 항상 변경에 대한 영향을 암시하기 때문. 하지만 JDK 표준 컬렉션 라이브러리 등에 속하는건 수정될 확률은 0에 가깝기 때문에 직접 생성하더라도 문제 없음