it 책/오브젝트: 코드로 이해하는 객체지향 설계
오브젝트: 코드로 이해하는 객체지향 설계 11장을 읽으며
lgvv
2024. 11. 17. 14:10
오브젝트: 코드로 이해하는 객체지향 설계 11장을 읽으며
느낀점
프로그래밍 언어의 발전과 구조에 대한 이론에 점점 더 흥미가 생기는 것 같음. 언어마다 다중 상속을 지원하지 않거나 mixin을 제공하는 방식을 보며 각 언어가 선택한 설계 철학과 구조의 차이를 이해하는 데 많은 인사이트를 얻음. 이러한 차이점은 단순히 문법적 요소에 국한되지 않고, 언어가 지향하는 객체지향의 특성과 설계 원칙에 깊이 뿌리를 두고 있다는 점이 체계적으로 언어를 잘 만든 것 같음.
합성과 유연한 설계
상속이 부모 클래스를 자식 클래스를 연결해서 부모 클래스의 코드를 재사용하는 데 비해 합성은 전체를 표현하는 객체가 부분을 표현하는 객체를 포함해서 부분 객체의 코드를 재사용
- 상속에서 부모와 자식 클래스 간의 관계는 컴파일 타임에 해결되지만, 의존성은 런타임에 해결된다
- 상속은 is-a 관계라고 부르고 합성 관계는 has-a 관계라고 부름
- 합성은 구현에 의존하지 않는다는 점에서 내부 구현이 변경되더라도 영향을 최소화할 수 있기 때문에 변경에 더 안정적인 코드를 얻을 수 있음.
- 상속 관계는 클래스 사이의 정적인 관계인 데 비해 합성 관계는 객체 사이의 동적인 관계
- 상속보다 합성을 이용하는 거싱 구현 관점에서 좀 더 번거롭고 복잡
- 변경에 유연하게 대처할 수 있는 설계가 대부분의 경우에 정답일 가능성이 높음.
- 클래스 사으의 높은 결합도를 객체 사이의 낮은 결합도로 대체할 수 있는 것.
상속을 합성으로 변경하기
오버라이딩한 인스턴스 메서드에서 내부의 인스턴스 메서드에서 내부의 인스턴스 호출을 그대로 전달할 수 있음
- 이를 포워딩(forwarding)이라고 부르고 동일한 메서드를 호출하기 위해 추가된 메서드를 포워딩 메서드(forwarding method)이라고 함.
- 기존 클래스의 인터페이스를 그대로 외부에 제공하면서 구현에 대한 결합 방식을 결합 없이 변경하고 싶은 경우 사용할 수 있는 유용한 기법
- 몽키 패치(Monkey Patch)는 현재 실행 중인 환경에만 영향을 미치도록 지역적으로 코드를 수정하거나 확장하는 것을 가리킴.
- 구현이 아니라 인터페이스에 의존하며하면 설계가 유연해짐
struct InstrumentedHashSet<E> {
private var addCount: Int = 0
private var `set`: Set<E>
// ✅ set 데이터에서 구현한 것을 호출함으로써 포워딩이라고 함.
func add(e: E) {
`set`.add(e)
}
}
상속으로 인한 조합의 폭발적인 증가
상속으로인해 하나의 기능을 추가할 때 필요한 작업의 양이 과도하게 늘어날 수 있음
- 하나의 기능을 추가하거나 수정하기 위해 불필요하게 많은 수의 클래스를 추가허거나 수정해야 함.
- 단일 상속만 지원하는 언어에서는 상속으로 인해 오히려 중복 코드의 양이 늘어날 수 있음.
- 상속의 남용으로 하나의 기능을 추가하기 위해 필요 이상으로 많은 수의 클래스를 추가해야 하는 경우를 가리켜 클래스 폭발(class explosion) 문제 또는 조합의 폭발(combinational explosion)이라고 함
- 이를 해결할 수 있는 최선의 방법은 상속을 포기하는 것
추상 메서드와 훅 메서드
개방-폐쇄 원칙을 만족하는 설계를 만들 수 있는 한 가지 방법은 부모 클래스에 새로운 추상 메서드를 추가하고 부모 클래스에 추상 메서드를 추가하고 부모 클래스의 다른 메서드 안에서 호출하는 것
- 추상 메서드의 단점은 상속 계층에 속하는 모든 자식 클래스가 추상 메서드를 오버라이딩 해야한다는 것.
- 단일 상속만 지원하는 언어에서는 상속으로 인해 오히려 중복 코드의 양이 늘어날 수 있음.
- 추상 메서드와 동일하게 자식 클래스에서 오버라이딩 할 의도로 메서드를 추가했지만 편의를 위해 기본 구현을 제공하는 메서드를 훅 메서드(hook method)라고 함
class Phone {
var fee: Money
func afterCalculated(fee: Money) -> Money {
return fee
}
}
class iPhone: Phone {
func afterCalculated(fee: Money) -> Money {
super.afterCalculated(fee: fee)
}
}
class Galaxy: Phone {
func afterCalculated(fee: Money) -> Money {
super.afterCalculated(fee: fee)
}
}
합성 관계로 변경
상속 관계는 컴파일타임에 결정되고 고정되기 때문에 코드를 실행하는 도중에는 변경할 수 없음
- 추상 메서드의 단점은 상속 계층에 속하는 모든 자식 클래스가 추상 메서드를 오버라이딩 해야한다는 것.
- 단일 상속만 지원하는 언어에서는 상속으로 인해 오히려 중복
- 해당 구조는 단일 책임 원칙을 준수
객체의 합성이 클래스 상속보다 더 좋은 방법
객체지향에서 가장 널리 사용되는 방법이 합성
- 코드를 재사용 하면서도 건전한 결합도를 유지할 수 있는 더 좋은 방법은 합성을 이용하는 것
- 그렇다면 상속은 사용해서는 안되는 것일까? 상속을 사용해야 하는 경우는 언제일까?
- 이번 장에서 살펴본 상속에 대한 모든 단점들은 구현 상속에 국한된다는 점 또한 이해야하 함
- 인터페이스 상속을 사용할 것
- 믹스인 기법: 상속과 합성의 특성을 모두 보유하고 있는 독특한 코드 재사용 방법
- 믹스인을 이해하고 나면 상속과 합성의 장단점에 대해 좀 더 깊이 이해하게 될 것
믹스인
부모 클래스와 자식 클래스가 강하게 결합되기 때문에 수정과 확장에 취약한 설계를 낳음.
- 코드를 재사용하면서도 납득할 만한 결합도를 유지하는 것
- 믹스인(mixin)은 객체를 생성할 때 코드 일부를 클래스 안에 섞어 넣어 재사용하는 기법을 가리키는 용어
- 합성: 실행 시점에 객체를 조합하는 재사용 방법
- 믹스인: 컴파일 시점에 필요한 코드 조각을 조합하는 재사용 방법
- 상속은 is-a 관계를 만들기 위한 것.
- 반면 믹스인은 말 그대로 코드를 다른 코드 안에 섞어 넣기 위한 방법
- 스칼라 언어에서는 트레이트(trait)을 이용해 믹스인 구현