역할, 책임, 협력책임 할당 시 고려해야 할 요소책임 주도 설계책임주도 설계의 대안응집도, 결합도, 캡슐화의존성(결합도) 관리하기높은 응집도와 낮은 결합도 (예시 1)데이터 중심 설계의 문제점자바의 JPA 사용 시 이슈퍼블릭 인터페이스 설계 원칙결합도와 응집도의 충돌객체지향 5대원칙(SOLID)OCP(개방폐쇄원칙)생성과 사용을 분리DIP(의존성 역전 원칙)의존성 역전 원칙과 패키지유연성에 대한 조언상속과 합성(서브클래싱으로서의) 상속을 사용할 때 생길 수 있는 문제(서브클래싱으로서의) 상속으로 인한 피해를 최소화 하는 방법상속과 합성의 차이상속으로 인한 조합의 폭발적인 증가 ⇒ 조합이 필요한 경우 합성사용해라상속의 사용 목적 두 가지(서브 클래싱과 서브 타이핑)상속은 어떤 목적으로 사용해야 하는가? ⇒ 서브 타이핑서브 타이핑 구성방법1. 행동 호환성 충족2. 행동 호환성 만족하지 않을 시, 클라이언트의 기대에 따라 계층 분리하기 (ISP - 인터페이스 분리 원칙)3. 리스코프 치환원칙(LSP) 충족 (↔ 행동호환성)디자인 패턴에 대한 팁들느낀점
역할, 책임, 협력
객체지향 설계의 핵심은
협력
을 구성하기 위해 적절한 객체를 찾고 적절한 책임
을 할당하는 과정에서 드러난다
객체지향 설계에서는 하나의 기능을 구현하기 위해 여러 객체들 사이에 메시지를 주고받으면서
협력
을 진행하고
이렇게 하나의 기능을 구현하며 객체가 협력에서 맡은 부분을 책임
이라 한다.- 객체의 책임 = 객체가 무엇을 알고 있는가 + 무엇을 할 수 있는가
객체가 협력 안에서 수행하는 책임들이 모여서 객체가 수행하는
역할
을 구성하는데, 예를 들어 위 예시에서 Movie가 DiscountPolicy 라는 인터페이스에 의존한다면 해당 인터페이스는 역할
이고, AmountDiscountPolicy, PercentDiscountPolicy 와 같은 각각의 구현 클래스들이 책임
이 되는 것이다. 협력을 설계하면서 객체의 책임을 식별해 나가는 과정에서 최종적으로 얻게 되는 결과물은 시스템을 구성하는 객체들의 인터페이스와 오퍼레이션의 목록이다.
책임 할당 시 고려해야 할 요소
- 메시지가 객체를 결정한다 (필요한 메시지를 먼저 식별하고 메시지를 처리할 객체를 나중에 선택)
- 행동이 상태를 결정한다
- 객체지향 패러다임에 갓 입문한 사람들이 가장 쉽게 빠지는 실수는 객체의 행동이 아니라 상태에 초점을 맞추는 것임. 초보자들은 먼저 객체에 필요한 상태가 무엇인지를 결정하고, 그 후에 상태에 필요한 행동을 결정한다. 이런 방식은 객체의 내부 구현이 객체의 퍼블릭 인터페이스에 노출되도록 만들기 때문에 캡슐화를 저해한다. 객체의 내부 구현을 변경하면 퍼블릭 인터페이스도 함께 변경되고, 결국 객체에 의존하는 클라이언트로 변경의 영향이 전파된다. 레베카 워프스브록은 이와 같이 객체의 내부 구현에 초점을 맞춘 설계 방법을
데이터 주도 설계
라고 부르기도 했다. - JPA를 쓰면 데이터 주도 설계가 되기 쉬움. 그래서 그 의존성을 끊으려면 새로운 클래스로 해당 클래스를 wrapping or 변환 해주어야 함
- 나에게 와닿았던 것은 객체지향 설계는 각 객체별로 책임을 나누어야 한다는 것(이로써 테스트도 쉬워지고, SRP도 충족되고)
- 그러나, 내가 익숙해져야 할 것은 각 객체간에는 메시지만을 호출한다는 것(=인터페이스)이고 그 메시지에는 구현 상세가 포함되어 있지 않아야 한다는 것임 (캡슐화)
책임 주도 설계
- 시스템이 사용자에게 제공해야 하는 기능인 시스템 책임을 파악
- 시스템 책임을 더 작은 책임으로 분할
- 분할된 책임을 수행할 수 있는 적절한 객체 또는 역할을 찾아 책임을 할당
- 객체가 책임을 수행하는 도중 다른 객체의 도움이 필요한 경우 이를 책임질 적절한 객체 또는 역할을 찾는다.
- 해당 객체 또는 역할에게 책임을 할당 함으로써 두 객체가 협력하게 한다.
책임 주도 설계의 핵심은 책임을 결정한 후(주고 받을 메시지를 정하는 것)에 책임을 수행할 객체를 결정하는 것임
책임주도 설계의 대안
- 책에서는 책임 주도 설계에 대한 상세한 가이드가 나오지만 아직까지 나에게 맞는(더 끌리는..) 방법은 일단 돌아가는 코드를 작성하고 난 후, 코드 상에 명확하게 드러나는 책임을 올바른 위치로 이동시키는 방법임
- 객체로 책임을 분배할 때 가장 먼저 할 일은 메서드를 응집도 있는 수준으로 분해하는 것
- 메서드 응집도 : 메서드가 명령문들의 그룹으로 구성되고 각 그룹에 주석을 달아야 할 필요가 있다면 그 메서드의 응집도는 낮은 것임
- 주석을 추가하는 대신 메서드를 작게 분해해서 각 메서드의 응집도를 높여라
- 중요한 것은 메서드의 이름과 메서드 몸체의 의미적 차이다.
- 메서드를 분리하고 나면 public 메서드는 상위 수준의 명세를 읽는 것 같은 느낌이 듬
응집도, 결합도, 캡슐화
캡슐화 준수 시 → 모듈 안의 응집도 상승(상태와 행동이 하나의 객체 안에 모이기에), 모듈 사이의 결합도는 하락(인터페이스로만 의존하기 때문에)
캡슐화 위반 시 → 모듈 안의 응집도 하락(내부의 정보가 바깥으로 새어 나가기 때문에, 변경이 필요할 시 내부와 외부를 다 바꿔주어야 하게 됨), 모듈 사이 결합도 상승( 마찬가지로 내부 구현이 바깥으로 흘러 나오기 때문에 내부 구현이 바뀌는 상황에 대해 외부 클라이언트도 바뀌어야 하므로 결합도가 상승하는 것 )
응집도
: 하나의 기능에 대해 변경이 일어날 때 그 객체의 모든 부분이 유기적으로 다 같이 바뀐다면 응집도가 높은 것이고, 하나의 기능에 대해 변경이 일어날때, 그 객체의 일부분만 변경된다면 응집도가 낮은 것임- 하나의 변경에 대해 하나의 모듈만 변경된다면 응집도가 높지만 다수의 모듈이 함께 변경돼야 한다면 응집도가 낮은 것임
캡슐화
: 상태와 행동을 하나의 객체 안에 모아서, 객체의 내부 구현을 외부로부터 감추는 것. 진정한 캡슐화는 변경될 수 있는 어떠한 것이라도 감추는 것을 의미함. 내부 구현의 변경으로 외부가 영향을 받는다면 캡슐화를 위반한 것임. 설계에서 변하는 것이 무엇인지 고려하고 변하는 개념을 캡슐화 해야 함- 상대적으로 변경 가능성이 높은 구현을 내부로 감추고 상대적으로 변경 가능성이 낮은 안전한 인터페이스 만을 외부로 노출함으로써, 변경의 영향을 통제할 수 있음
- 객체는 다른 객체의 상세한 내부 구현에 직접 접근할 수 없기 때문에 오직 메시지 전송(=인터페이스 오퍼레이션)을 통해서만 자신의 요청을 전달할 수 있음 → 캡슐화
- Screening이 Movie에게 calculateMovieFee 메시지를 전송하는 이유는, 요금을 계산하는 데 필요한 기본 요금과 할인 정책을 가장 잘 알고 있는 객체가 Movie이기 때문임(INFORMATION EXPERT 패턴) → 캡슐화
- 상태와 행동을 객체 라는 하나의 단위로 묶는 이유는 객체 스스로 자신의 상태를 처리할 수 있게 하기 위해서임
결합도
: 한 모듈이 변경되기 위해서 다른 모듈의 변경을 요구하는 정도
의존성(결합도) 관리하기
- 결합도의 정도는 한 요소가 자신이 의존하고 있는 다른 요소에 대해 알고 있는 정보의 양으로 결정됨
Movie
가PercentDiscountPolicy
에 의존한다고 가정해보면Movie
는 협력할 객체가 비율 할인 정책에 따라 할인 요금을 계산할 것이라는 사실을 알고 있는 것- 반면 DiscountPolicy 클래스에 의존하는 경우에는 구체적인 계산 방법은 알 필요가 없이, 할인 요금을 계산한다는 사실만 알고 있음
- 결합도를 낮추려면 추상화에 의존하면 됨. 인터페이스 > 추상 클래스 > 구체 클래스, 순서대로 결합도가 낮음
추상화에 의존하게 되면 구체적인 컨텍스트에 매이지 않기에 컨텍스트 확장이 가능함(=개방폐쇄원칙. 변경에는 닫혀있고 확장에는 열려있다). 예를 들어 Movie가 DiscountPolicy라는 인터페이스에 의존하게 되면 비율할인정책, 금액할인정책, 중복할인정책, 할인혜택제공x 의 경우에 대해 동일한 의존성으로 구현이 가능함
높은 응집도와 낮은 결합도 (예시 1)

- 설계는 트레이드오프 활동이라는 것을 기억하라. 동일한 기능을 구현할 수 있는 무수히 많은 설계가 존재한다. 따라서 실제로 설계를 진행하다 보면 몇 가지 설계 중 한가지를 선택해야 하는 경우가 빈번하게 발생한다.
- 예로, 위의 영화 예매 시스템에서 할인 요금 계산하기 위해 Movie가 DiscountCondition에 할인 여부를 판단하라는 메시지를 전송함
- 이 설계의 대안으로 Screening이 직접 DiscountCondition에 할인 여부 판단하라고 메시지 보내고, Movie에게 가격 계산하라는 식으로 메시지 보낸다면?
- 기능은 동일하지만, 도메인 개념을 참고해보면 Movie가 DiscountCondition의 리스트를 갖고 있기에 Movie가 메시지를 보내는 편이 결합도를 낮추게 된다.(Screening이 굳이 DiscountCondition을 몰라도 되기에) ⇒
LOW COUPLING
- 또한, Screening의 가장 중요한 책임은 예매를 생성하는 것인데, 만약 Screening이 DiscountCondition과 협력해야 한다면 Screening은 영화 요금 계산과 관련된 책임 일부를 떠안아야 할 것임. 이 경우 Screening은 DiscountCondition이 할인 여부를 판단할 수 있고 Movie가 이 할인 여부를 필요로 한다는 사실 역시 알고 있어야 한다. ⇒ 예매 요금 계산 방식이 변경될 경우 Screening도 함께 변경 → 응집도가 낮아짐(
HIGH COHESION
X
) - Movie 의 주된 책임이 영화 요금을 계산하는 것이기에 Discount Condition과 협력하는 것이 응집도에 아무런 해도 끼치지 않는다.

데이터 중심 설계의 문제점
- 데이터 중심 설계가 변경에 취약한 이유
- 데이터 중심 설계는 본질적으로 너무 이른 시기에 데이터에 관해 결정하도록 강요한다 ( 데이터는 구현의 일부!!!)
- 데이터 중심 설계에서 객체는 그저 단순한 데이터의 집합체임. 따라서 객체의 행동보다는 상태에 초점을 맞추게 됨
- 데이터를 먼저 결정하고 데이터를 처리하는 데 필요한 오퍼레이션을 나중에 결정하는 방식은 데이터에 관한 지식이 객체의 인터페이스에 고스란히 드러나게 되어 캡슐화가 실패하게 됨
- 협력이라는 문맥을 고려하지 않고 객체를 고립시킨 채 오퍼레이션을 결정함
- 데이터 중심설계에서 초점은 객체의 외부가 아니라 내부로 향함. 실행 문맥에 대한 깊이 있는 고민 없이 객체가 관리할 데이터의 세부 정보를 먼저 결정함. 객체의 구현이 이미 결정된 상태에서 다른 객체와의 협력 방법을 고민하기 때문에 이미 구현된 객체의 인터페이스를 억지로 끼워맞출 수 밖에 없음
자바의 JPA 사용 시 이슈
- JPA 를 사용하게 되면 Entity 객체를 여기저기서 많이 사용하게 되고 이 클래스를 기반으로 설계가 진행되게 된다. 따라서 데이터 중심 설계가 되고 변경에 매우 취약한 시스템이 된다.
- eg. 예를 들어 Entity 에 필드 하나 추가한다고 하자(db에 컬럼 추가). 그러면 해당 Entity를 이용한 테스트, 그 위의 리포지토리, 서비스, 컨트롤러 레이어까지 싹 다 변경해주어야 함. 결합도, 캡슐화, 응집도 다 떨어짐 ⇒ 도메인 계층과 영속성 계층 각각에서 엔티티를 만들고 각각의 계층이 데이터 주고 받을 때 두 엔티티를 서로 변환해야 함
- 즉, 서비스 레이어에서 JPA에 의존성을 갖지 않도록 중간에 인터페이스를 두고 인터페이스로만 영속성 레이어를 호출하고 영속성 레이어는 그 인터페이스를 구현하는 식으로 코딩하기
- 육각형 아키텍처 도입이 필요할까..
퍼블릭 인터페이스 설계 원칙
- 좋은 인터페이스는
최소한의 인터페이스
와추상적인 인터페이스
라는 조건을 만족해야 한다 - 최소한의 인터페이스는 꼭 필요한 오퍼레이션만을 인터페이스에 포함하고, 추상적인 인터페이스는 어떻게 수행하는지가 아니라 무엇을 하는지를 표현한다.
- 퍼블릭 인터페이스의 품질에 영향을 미치는 원칙과 기법
- 수행 방법(구현)에 관해 언급하지 말고 결과와 목적만을 포함하도록 클래스와 오퍼레이션의 이름을 부여하라
- 명령-쿼리 분리 : 어떤 오퍼레이션이 부수효과를 발생시키는 명령(객체 내부 상태 수정)이거나, 부수효과를 발생시키지 않는 쿼리(객체 정보 반환) 중 하나여야 한다는 것임. 어떤 오퍼레이션도 명령인 동시에 쿼리여서는 안됨
디미터 법칙 : 객체의 내부 구조를 물어서 너(클라이언트)가 하지 말고 그냥 시켜라
캡슐화 원칙이 클래스 내부의 구현을 감춰야 한다는 사실을 강조한다면 디미터 법칙은 협력하는 클래스의 캡슐화를 지키기 위해 접근해야 하는 요소를 제한한다.
묻지 말고 시켜라 : 상태를 묻는 오퍼레이션을 행동을 요청하는 오퍼레이션으로 대체함으로써 인터페이스를 향상 시켜라
절차적인 코드는 정보를 얻은 후에 결정하지만, 객체지향 코드는 객체에게 그것을 하도록 시킨다.
의도를 드러내는 인터페이스 : 메서드의 이름을 메서드가 어떻게 작업을 수행하는지(구현)x
를 나타내도록 이름 짓지 말고, 무슨 작업을 수행하는지 o
를 나타내도록 지어라
결합도와 응집도의 충돌
설계는 트레이드오프의 산물이기에 원칙이 현재 상황에 부적합하다고 판단되면 과감하게 원칙을 무시해야 하고, 원칙을 아는 것보다 더 중요한 것이 언제 원칙이 유용하고 유용하지 않은지를 판단할 수 있는 능력을 기르는 것임
- 위 상황에서 Screening의 내부 구현을 PeriodCondition에서 다 안다고 해서 캡슐화를 위반한 것으로 보일 수 있고 해당 구현을 Screening으로 옮겨버리면 묻지 말고 시켜라 스타일을 준수하는 퍼블릭 인터페이스를 얻을 수 있다고 생각할 수 있다.
- 그러나 이렇게 되면 Screening이 기간에 따른 할인 조건을 판단하는 책임을 떠안게 되고 이는 Screening 이 담당해야 할 본질적 책임이 아니기에 응집도를 낮추게 되는 결과로 이어진다.
- 또한, Screening이 PeriodCondition의 인스턴스 변수를 인자로 받기 때문에 PeriodCondition의 인스턴스 변수 목록이 변경될 경우에도 영향을 받게 되어 Screening과 PeriodCondition의 결합도를 높이게 된다.
- 따라서, Screening의 캡슐화를 향상시키는 것보다 Screening의 응집도를 높이고 Screening과 PeriodCondition 사이의 결합도를 낮추는 것이 전체적인 관점에서 더 좋은 방법이다
객체지향 5대원칙(SOLID)
OCP(개방폐쇄원칙)
- 의존성 관점에서 개방-폐쇄 원칙을 따르는 설계는 컴파일타임 의존성은 유지하면서 런타임 의존성의 가능성을 확장하고 수정할 수 있는 구조
- 개방-폐쇄 원칙의 핵심은
추상화에 의존하는 것
임
- 의존성(결합도) 관리하기 에서 본 것처럼 Movie가 DiscountPolicy라는 추상화에 의존하는 것이 개방폐쇄원칙을 지킬 수 있도록 해주는 것임
생성과 사용을 분리
- 유연하고 재사용 가능한 설계를 원한다면 객체와 관련된 두 가지 책임(객체의 생성, 객체의 사용)을 서로 다른 객체로 분리해야 함
Movie는 DiscountPolicy를 사용
하고, Movie에게 DiscountPolicy 타입의 객체를 생성
해서 넘겨주는 것은 클라이언트
- 사용을 하는 입장(Movie)에서는 추상화(DiscountPolicy)에 의존하여 컨텍스트에 독립적으로 사용을 하고 클라이언트(Movie를 사용하는 클라이언트)가 컨텍스트(AmountDiscountPolicy, PercentDiscountPolicy , …)에 대한 결정권을 가지고 의존성 해결을 해줌
DIP(의존성 역전 원칙)
- 상위 수준의 모듈은 하위 수준의 모듈에 의존해서는 안 된다. 둘 모두 추상화에 의존해야 한다.
- 위의 예시에서 Movie가 AmountDiscountPolicy라는 구체적인 하위 수준 모듈에 의존한다면 할인정책을 바꾼다고 할때(=구현이 바뀌면) 변경이 전파됨
- 따라서 Movie는 DiscountPolicy라는 추상화에 의존해야 하고 AmountDiscountPolicy도 DiscountPolicy에 의존해야 함(상속)

- 추상화(DiscountPolicy)는 구체적인 사항(AmountDiscountPolicy, PercentDiscountPolicy)에 의존해서는 안 된다. 구체적인 사항이 추상화에 의존해야 한다.
좋은 객체지향 설계의 증명이 바로 이와 같은 의존성의 역전이다. 프로그램의 의존성이 역전돼 있다면, 이것은 객체지향 설계를 갖는 것이다. 의존성이 역전돼 있지 않다면, 절차적 설계를 갖는 것이다. 이 원칙은 또한 변경에 탄력적인 코드를 작성하는 데 있어 결정적으로 중요하다. 추상화와 구체적인 사항이 서로 고립돼 있기 때문에 이 코드는 유지보수하기가 훨씬 쉽다 — 로버트 마틴
의존성 역전 원칙과 패키지


- Movie와 DiscountPolicy를 하나의 패키지로 모음으로써 Movie를 특정한 컨텍스트(할인정책의 종류)로부터 완벽하게 독립 시킬 수 있게 되고 이는 상위 수준의 협력 흐름을 재사용 하기 쉽게 만들어줌
유연성에 대한 조언
유연한 설계
라는 말의 이면에는 복잡한 설계
라는 의미가 숨어있기에 아직 일어나지 않은 변경에 대한 불안감으로 불필요하게 복잡한 설계를 하지 않아야 한다(불필요한 유연성은 불필요한 복잡성을 낳는다
) 만약 단순하고 명확한 해법이 그런대로 만족스럽다면 유연성을 제거하라. 유연성은 코드를 읽는 사람들이 복잡함을 수용할 수 있을 때만 가치가 있다.
상속과 합성
(서브클래싱으로서의) 상속을 사용할 때 생길 수 있는 문제
상속을 위한 경고
1. 취약한 기반 클래스 문제. 상속은 자식 클래스가 부모 클래스의 구현 세부사항에 의존하도록 만들기 때문에 캡슐화를 약화시킴
2. 상속받은 부모 클래스의 메서드가 자식 클래스의 내부 구조에 대한 규칙을 깨트릴 수 있음(
java.util.Stack
→ java.util.Vector
, java.util.Properties
→ java.util.Hashtable
)
3. 자식 클래스가 부모 클래스의 메서드를 오버라이딩할 경우 부모 클래스가 자신의 메서드를 사용하는 방법에 자식 클래스가 결합될 수 있다 (InstrumentedHashSet
의 예시)
4. 클래스를 상속하면 결합도로 인해 자식 클래스와 부모 클래스의 구현을 영원히 변경하지 않거나, 자식 클래스와 부모 클래스를 동시에 변경하거나 둘 중 하나를 선택할 수 밖에 없다. 상속은 자식 클래스를 점진적으로 추가해서 기능을 확장하는 데는 용이하지만 높은 결합도로 인해 부모 클래스를 점진적으로 개선하는 것을 어렵게 만든다. 최악의 경우에는 모든 자식 클래스를 동시에 수정하고 테스트해야 할 수도 있다.
(서브클래싱으로서의) 상속으로 인한 피해를 최소화 하는 방법
1. 상속받은 자식 클래스 사이에 두 메서드가 유사하게 보인다면 차이점을 메서드로 추출하라. 메서드 추출을 통해 두 메서드를 동일한 형태로 보이도록 만들 수 있다
2. 부모 클래스의 코드를 하위로 내리지 말고 자식 클래스에서 중복되는 코드를 상위로 올려라. 부모 클래스의 구체적인 메서드를 자식 클래스로 내리는 것보다 자식 클래스의 추상적인 메서드를 부모 클래스로 올리기
템플릿 메서드 패턴
- 여러 자식 클래스에서 차이가 나는 부분을 메서드로 분리하고 공통된 부분을 부모 클래스로 올리기
- 그리고 차이나는 부분의 메서드를 부모 클래스에 추상 메서드로 선언
- 각각의 자식 메서드에서 추상 메서드를 구현
위와 같이 구현하면 각 클래스는 서로 다른 변경의 이유를 가지게 되어 단일 책임 원칙을 준수하고 응집도 또한 높아지게 된다.( 변경의 주기가 다르다 ↔ 각각의 모듈의 변경의 이유가 다르다는 것 → 응집도가 높고, 단일 책임 원칙이 적용된 것)
AbstractPhone
: 전체 통화 목록 계산 방법이 바뀔 때만 변경됨
Phone
: 일반 요금제의 통화 한 건을 계산하는 방식이 바뀔 경우에만 변경
NightlyDIscountPhone
: 심야 할인 요금제의 통화 한 건을 계산하는 방식이 바뀔 경우에만 변경
상속과 합성의 차이
ㅤ | 상속 | 합성 |
재사용방법 | 부모 클래스와 자식 클래스를 연결해서 부모 클래스의 코드를 재사용 (is-a 관계) | 전체를 표현하는 객체가 부분을 표현하는 객체를 포함해서 부분 객체의 코드를 재사용 (has -a 관계) |
결합도 | 상속을 제대로 활용하기 위해서는 부모 클래스의 내부 구현에 대해 상세하게 알아야 하기 때문에 자식 클래스와 부모 클래스 사이의 결합도가 높아질 수 밖에 없다. | 구현에 의존하지 않는다는 점에서 상속과 다름. 합성은 내부에 포함되는 객체의 구현이 아닌 퍼블릭 인터페이스에 의존. 따라서 포함되는 객체의 내부 구현이 변경되더라도 영향을 최소화 할 수 있기에 결합도가 떨어짐 |
관계 | 클래스 사이의 정적 관계 (컴파일 의존)
컴파일타임 의존성 = 런타임 의존성 | 객체 사이의 동적인 관계 (런타임 의존성)
런타임에 동적으로 변경가능 |
의존성 | 구현에 대한 의존성 | 인터페이스에 대한 의존성 |
표현 방식 | 화이트박스 재사용 (부모 클래스의 내부가 자식 클래스에 공개되기에) | 블랙박스 재사용 (내부는 공개되지 않고 인터페이스를 통해서만 재사용되기에) |
[코드 재사용을 위해서는] 객체 합성이 클래스 상속보다 더 좋은 방법이다.
상속으로 인한 조합의 폭발적인 증가 ⇒ 조합이 필요한 경우 합성사용해라
가장 일반적인 상황은 작은 기능들을 조합해서 더 큰 기능을 수행하는 객체를 만들어야 하는 경우다. 부가 정책이 추가 되는 조합 별로 자식 클래스가 추가되어야 하기 때문에(상속을 이용하는 경우) 필요 이상으로 많은 수의 클래스가 생겨 나는 것을
클래스 폭발
or 조합의 폭발
문제라고 부름 ⇒ 합성 관계로 변경시 해결 가능
위의 그림과 같이 합성 관계로 Phone과 RatePolicy 사이의 관계를 맺게 되면 정책 별로 자식 클래스(NightlyDiscountPhone, RegularPhone …)를 만들 필요 없이 클라이언트에서 해당하는 정책으로 의존성을 해결해주기만 하면 유연하게 동작할 수 있게 된다.
상속의 사용 목적 두 가지(서브 클래싱과 서브 타이핑)
코드 재사용
(서브 클래싱
↔ 구현 상속 ↔ 클래스 상속 ) : 자식 클래스와 부모 클래스의 행동이 호환되지 않기 때문에 자식 클래스의 인스턴스가 부모 클래스의 인스턴스를 대체할 수 없음- 서브 클래싱은 클래스의 내부 구현 자체를 상속받는 것에 초점을 맞춤 ( 구현 상속, 클래스 상속 )
타입 계층을 구성
(서브 타이핑
↔ 인터페이스 상속 ) : 타입 계층을 구성하기 위해 상속을 사용하는 경우 서브 타이핑에서는 자식 클래스와 부모 클래스의 행동이 호환되기 때문에 자식 클래스의 인스턴스가 부모 클래스의 인스턴스 대체 가능- 서브 타이핑 관계가 유지되기 위해서는 서브타입이 슈퍼타입이 하는 모든 행동을 동일하게 할 수 있어야 함. 즉
행동 호환성
을 만족 시켜야 함 - 다시 말해 자식 클래스와 부모 클래스 사이의
행동 호환성
은 부모 클래스에 대한 자식 클래스의대체 가능성
을 포함함
상속은 어떤 목적으로 사용해야 하는가? ⇒ 서브 타이핑
코드 재사용의 목적(
서브 클래싱
)이 아닌 타입 계층(서브 타이핑
)을 구성하기 위한 목적으로 사용해야 함서브 타이핑 구성방법
1. 행동 호환성 충족
클라이언트의 관점에서 두 타입이 동일하게 행동(
행동호환성
)한다고 기대될 때만 타입 계층으로 묶어야 한다.- 펭귄이 새가 아니라는 사실을 받아들이기 위한 출발점은 타입이 행동과 관련이 있다는 사실에 주목하는 것
- Penguin은 클라이언트에게 Bird와 행동 호환이 되지 않기에 Bird의 서브타입이 아니다.
2. 행동 호환성 만족하지 않을 시, 클라이언트의 기대에 따라 계층 분리하기 (ISP - 인터페이스 분리 원칙)
행동 호환성을 만족시키지 않는 상속 계층을 그대로 유지한 채 클라이언트의 기대를 충족시킬 수 있는 방법은 쉽지 않다 → 클라이언트의 기대에 맞게 상속 계층을 분리해야 한다.
- flyBird 메서드는 FlyingBird 타입을 이용해 날 수 있는 새만 인자로 전달돼야 한다는 사실을 코드에 명시할 수 있음. 만약 날 수 없는 새와 협력하는 메서드가 존재한다면 파라미터의 타입을 Bird로 선언하면 됨

- 인터페이스는 클라이언트가 기대하는 바에 따라 분리돼야 한다는 것을 기억하기
- 하나의 클라이언트가 오직 fly 메시지만 전송하기를 원한다면 이 클라이언트에게는 fly 메시지만 보여야 함
- 다른 클라이언트가 오직 walk 메시지만 전송하기를 원한다면 이 클라이언트에게는 walk 메시지만 보여야 함
ISP가 잘 적용됐을때의 이점
- 클라이언트에 따라 인터페이스를 분리하면 변경에 대한 영향을 더 세밀하게 제어할 수 있게 된다.
- 클라이언트에 따라 인터페이스를 분리하면 각 클라이언트의 요구가 바뀌더라도 영향의 파급 효과를 효과적으로 제어할 수 있게 된다.
인터페이스 분리원칙은 ‘비대한’ 인터페이스의 단점을 해결한다. 비대한 인터페이스를 가지는 클래스는 응집성이 없는 인터페이스를 가지는 클래스다. 즉, 이런 클래스의 인터페이스는 메서드의 그룹으로 분해될 수 있고, 각 그룹은 각기 다른 클라이언트 집합을 지원한다.
3. 리스코프 치환원칙(LSP) 충족 (↔ 행동호환성)
리스코프 치환 원칙 한마디로 →
“클라이언트가 차이점을 인식하지 못한 채 기반 클래스의 인터페이스를 통해 서브클래스를 사용할 수 있어야 한다”
- 위의 행동호환성을 설계원칙으로 정리한 것이 리스코프 치환원칙임
- 자식 클래스가 부모 클래스와
행동 호환성
을 유지함으로써 부모 클래스를 대체할 수 있도록 구현된 상속 관계만을서브타이핑
이라고 불러야 함
- 리스코프 치환 원칙은 클라이언트와 격리한 채로 모델을 의미 있게 검증하는 것이 불가능하다는 아주 중요한 결론을 이끈다. 어떤 모델의 유효성은 클라이언트의 관점에서만 검증 가능하다.
- 자식 클래스가 클라이언트의 관점에서 부모 클래스를 대체할 수 있다면 기능 확장을 위해 자식 클래스를 추가하더라도 코드를 수정할 필요가 없어진다. 따라서 리스코프 치환 원칙은 개방-폐쇄 원칙을 만족하는 설계를 위한 전제 조건이다.
디자인 패턴에 대한 팁들
- 어떤 구현 코드가 어떤 디자인 패턴을 따른다고 이야기할 때는 역할, 책임, 협력의 관점에서 유사성(템플릿처럼)을 공유한다는 것이지 특정한 구현방식을 강제하는 것은 아니라는 점을 이해하는 것이 중요함
- 디자인 패턴에서 중요한 것은 디자인 패턴의 구현 방법이나 구조가 아니다. 어떤 디자인 패턴이 어떤 변경을 캡슐화 하는지를 이해하는 것이 중요하다.
- TEMPLATE METHOD 패턴 : 변경하지 않는 부분은 부모 클래스로, 변하는 부분은 자식 클래스로 분리함으로써 변경을 캡슐화. 알고리즘을 캡슐화하기 위해 합성 관계가 아닌 상속 관계를 사용하는 것을 템플릿 메서드 패턴
- 부모 클래스가 알고리즘의 기본 구조를 정의하고 구체적인 단계는 자식 클래스에서 정의하게 함으로 변경을 캡슐화 할 수 있는 디자인 패턴. 다만 합성보다는 결합도가 높은 상속을 사용했기에 STRATEGY 패턴처럼 런타임에 객체의 알고리즘을 변경하는 것은 불가능함.
- 변경을 캡슐화한다 → 해당 변경이 내부에만 영향이 전파되어 외부에서는 변경이 필요 없는 것을 의미
- 패턴을 사용하면서 부딪히게 되는 대부분의 문제는 패턴을 맹목적으로 사용할 때 발생함. 컨텍스트의 적절성은 무시한 채 패턴의 구조에만 초점을 맞추는 것
- 패턴에 처음 입문한 사람들은 패턴의 강력함에 매료된 나머지 아무리 사소한 설계라도 패턴을 적용해 보려고 시도한다. 그러나 명확한 트레이드오프 없이 패턴을 남용하면 설계가 불필요하게 복잡해지게 된다.
느낀점
포함되어야 하는 내용
- 나에게 와닿았던 내용들 중심으로 정리한 것이기에 빠져있는 부분이 있음
- 요약을 하며 책의 구절을 발췌한 부분도 있고, 내가 생각해서 정리한 부분도 있다.