오브젝트: 코드로 이해하는 객체지향 설계 9장을 읽으며
오브젝트: 코드로 이해하는 객체지향 설계 9장을 읽으며
느낀점
개방 폐쇄 원칙 및 컴파일 타임 의존성, 런타임 의존성 보면서 기존 설계한 코드들을 다시 보게 되었음. 특히 Factory는 많이 사용하는데, 이거 어디에 위치시킬지 매번 고민했는데, 순수한 기술적 결정이므로 어디에 위치시킬지 명확한 근거를 알게 되었음. create, use를 분리하는거 경험적으로 행하고 있었는데, 문서로 보니까 더욱 좋았음!
유연할 설계
이전 장에서는 재사용 가능한 설계를 만들기 위해 적용할 수 있는 다양한 의존성 관리 기번을 소개. 이번에는 원칙이라는 관점에서 정리
- 개방 폐쇄 원칙은 확장과 수정이 키워드
- 확장에 대해 열려 있다: 애플리케이션의 요구사항이 변경될 때 이 변경에 맞게 새로운 동작을 추가해서 애플리케이션의 기능을 확장할 수 있음.
- 수정에 대해 닫혀 있다: 기존의 코드를 수정하지 않고도 동작을 추가하거나 변경할 수 있음.
컴파일 타임 의존성을 고정시키고 런타임 의존성을 변경하라
OCP는 런타임 의존성과 컴파일타임 의존성에 관한 이야기
추상화가 핵심이다
OCP의 핵심은 추상화에 의존하는 것
- 추상화란 핵심적인 부분만 남기고 불필요한 부분은 생략함으로써 복잡성을 극복하는 기법
- 추상화를 사용해 생략된 부분을 문맥에 적합한 내용으로 채워넣음으로써 각 문맥에 적합하게 기능을 구체화하고 확장할 수 있음.
- 추상화를 통해 확장의 여지를 남기며 추상화 된 부분은 수정에 닫혀 있음.
- 수정에 닫혀있다는 말은 공통된 부분은 문맥에 바뀌더라도 변하지 않아야 한다는 의미
- 변하는 것과 변하지 않는 것을 무엇인지 이해하고 이를 추상화의 목적으로 삼아야 함
생성 사용 분리
객체가 추상화에만 의존하기 위해서는 객체 내부에서는 구체 클래스(concreate class)의 인스턴스를 생성하면 안됨
- 결합도가 높아질수록 OCP를 따르는 구조를 설계하기가 어려워 짐
- 동일한 클래스 안에서 객체 생성과 사용이라는 두 가지 이질적인 목적을 가진 코드가 공존하는 것이 문제
- 유연하고 재사용 가능한 설계를 원한다면 객체와 관련된 두 가지 책임을 서로 다른 객체로 분리해야 함.
- 하나는 객체를 생성하는 것
- 다른 하나는 객체를 사용하는 것
- 이를 객체에 대한 생성과 사용을 분리(separating use from creation)해야 함
아래는 객체의 생성과 사용 책임을 분리
class Client {
func getFee() -> Money {
var avartar = Movie("아바타",
Duration.ofMinutes(120),
Money.wons(10000),
AmountDiscountPolicy())
return avartar.getFee()
}
}
FACTORY 추가하기
생성 책임을 특정 컨텍스트에 묶여서는 안되지만 위 설계는 Client에 묶여도 된다는 전제가 깔림. 이번에는 Client도 특정 컨텍스트에 묶이지 않는다고 가정
- 객체 생성과 관련된 지식이 Client와 협력하는 클라이언에게까지 새어나가지를 원하지 않는다고 가정
- 생성과 사용을 분리하기 위해 객체 생성에 특화된 객체를 FACTORY라고 부름.
- Factroy를 사용하면 Client는 오직 사용과 관련한 책임만 가지고 생성과 관련된 어떤 지식도 가지지 않을 수 있음.
class Factory {
func createAvatarMovie() -> Movie {
return Movie("아바타",
Duration.ofMinutes(120),
Money.wons(10000),
AmountDiscountPolicy())
}
}
class Client {
var factory: Factory
init(factory: Factory) {
self.factory = factory
}
func getAvartarFee() -> Money {
var avartar: Movie = factory.createAvatarMovie()
return avartar.getFee()
}
}
순수한 가공물에게 책임 할당하기
책임을 수행하는데 가장 정보를 많이 아는 정보 전문가에게 책임을 할당하는 것이며, 도메인 모델은 정보 전문가를 찾기 위해 참조할 수 있는 일차적 재료
- 위에서 추가한 FACTORY는 도메인 모델에 속하지 않음.
- FACTORY를 추가한 이유는 순가하게 기술적인 결정
- 도메인 개념과는 아무런 상관이 없는 가공의 객체로 이동
- 객체 분해에는 표현적 분해(representational decomposition)이고, 다른 하나는 행위적 분해(behavioral decomposition)
- 표현적 분해: 도멩니에 존재하는 개념을 표현하는 객체들을 이용해 시스템을 분해하는 것
- 도메인 모델에 담겨있는 개념과 관계를 따르며 도메인과 소프트웨어 사이의 표현적 차이를 최소화하는 것을 목적으로 함
- 모든 책임을 객체에게 할당하면 낮은 응집도, 높은 결합도, 재사용성 저하와 같은 심각한 문제점에 봉착하게 될 가능성이 높아짐.
- 도메인과 무관한 인공적인 객체를 PURE FABRICATION(순수한 가공물)이라 부름
- 어떤 행동을 추가하려고 하는데 이 행동을 책임질 마땅한 도메인 개념이 존재하지 않는다면, 순수한 가공물을 추가하고 이것에게 책임을 할당
- 순수한 가공물은 표현적 분해보다 행위보다 행위적 분해에 의해 생성되는 것이 일반적
의존성 주입
생성과 사용을 분리하면 오로지 인스턴스를 사용하는 책임만 남게 됨
- 외부에서 의존성을 주입한 후 이를 사용하는 객체 쪽으로 주입하기 때문에 의존성 주입이라고 부름
- 세가지 방법 존재
- 생성자 주입: 객체를 생성하는 시점에 생성자를 통한 의존성 해결
- setter 주입: 객체 생성 후에 setter 메서드를 통한 의존성 해결
- 메서드 주입: 메서드 실행 시 인자를 이용한 의존성 해결
// ✅ 생성자 주입
func createAvatarMovie() -> Movie {
return Movie("아바타",
Duration.ofMinutes(120),
Money.wons(10000),
AmountDiscountPolicy())
}
// ✅ setter 주입
avartar.setDiscountPolicy(AmountDiscountPolicy(...))
// ✅ 메서드 호출 주입
avartar.calcultaeDiscountAmount(screening, AmountDiscountPolicy(...))
숨겨진 의존성은 나쁘다
의존성 주입 외에도 의존성 해결할 다양한 방법이 존재
- 명시적인 의존성의 경우에는 깊은 계층을 따라 모두 주입해야한다는 불편함이 존재하나, 내부에 숨길수록 수정 및 이해가 어려워짐.
- 명시적인 의존성에 초점을 맞춰 유연성을 향상하는 가장 효과적인 방법
추상화와 의존성 역전
의존성 역전 원칙(DIP)는 전통적인 절차형 프로그래밍 언어와 반대로 나타남
- 상위 수준의 모듈은 하위 수준의 모듈에 의존하면 안됨
- 추상화는 구체적인 사항에 의존해서는 안되며, 구체적인 사항은 추상화에 의존
의존성 역전 원칙과 패키지
코드 수정이 아니라 컴파일 측면에서 알아보자
- 인터페이스가 서버 모듈쪽에 위치할 경우 DiscountPolicy가 포함된 패키지 안의 어떤 클래스가 수정되더라도 패키지 전체가 재배포 되어야 함
- 이로 인해 이 패키지에 의존하는 Movie 클래스가 포함된 패키지 역시 재컴파일 되어야 함
- 컴파일은 의존성 그래프를 타고 애플리케이션 전체로 재컴파일 되어야 함.
- 이는 전체적인 빌드 시간을 가파르게 상승 시킴
클라이언트가 속한 패키지에 인터페이스의 소유권을 포함시켜야 함. 재사용될 필요가 없는 클래스들은 별도의 독립적인 패키지에 모아야 함.
유연한 설계는 유연성이 필요할 때만 옳다
재사용 가능한 설계가 항상 좋은건 아니며 설계의 미덕은 단순함과 명확함으로부터 나옴
- 객체가 유연할수록 클래스 구조와 객체 구조 사이의 거리는 점점 멀어짐.
- 유연성은 항상 복잡성을 수반하며, 유연하지 않은 설계는 단순하고 명확함.
- 불필요한 유연성은 복잡성을 낳음.
- 협력과 책임이 중요하며, 객체를 생성하는 방법에 대한 결정은 모든 책임이 자리를 잡은 후 가장 마지막 시점에 내리는 것이 적절함.