IT 서적/오브젝트

오브젝트

수달하나 2023. 9. 12. 00:20


Chapter 9 유연한 설계

01 개방-폐쇄 원칙

 

개방-폐쇄에 대한 원칙 : 확장 가능하고 변화에 유연하게 대처할 수 있는 설계를 만드는 원칙 중 하나로 소프트웨어 개체는 확장에 대해 열려있어야 하고 수정에 대해서는 닫혀 있어야 한다.

 

컴파일타임 의존성을 고정시키고 런타임 의존성을 변경해야 한다.

유연하고 재사용 가능한 설계에서 런타임 의존성과 컴파일타임 의존성은 서로 다른 구조를 가진다.

기존 클래스를 전혀 수정하지 않은 채 어플리케이션의 동작을 확장할 수 있는 방법으로 확장에 대해서는 열려있는 설계를 진행해야 한다.

 

개방-폐쇄 원칙을 수용하는 코드는 컴파일타임 의존성을 수정하지 않고도 런타임 의존성을 쉽게 변경 할 수 있다.

 

추상화가 핵심이다.

추상화 부분은 수정에 대해 닫혀 있다. 추상화를 통해 생략된 부분은 확장의 여지를 남기기 때문에 추상화가 개방-폐쇄 원칙을 가능하게 만드는 이유다.

단순히 어떤 개념을 추상화 했다고 수정에 대해 닫혀있는 설계를 할 수 있는 것은 아니다.

개방-폐쇄 원칙에서 폐쇄를 가능하게 하는 것은 의존성의 방향이다.

수정에 대한 영향을 최소화 하기 위해서는 모든 요소가 추상화에 의존해야 한다.


02 생성 사용 분리

 

A 객체가 B라는 추상화에만 의존하기 위해서는 A 의 내부에서 B의 구체 클래스(자식 클래스)의 인스턴스를 생성해서는 안된다.

"생성" 과 "사용" 을 분리해야한다.

사용으로부터 생성을 분리하는 데 사용되는 가장 보편적인 방법은 객체를 생성할 책임을 클라이언트로 옮기는 것이다. 다시 말해 클라이언트가 인스턴스를 직접 생성한 후 전달하는 것이다.

인스턴스를 생성하는 책임을 클라이언트에 맡김으로써 구체적인 컨텍스트와 관련된 정보는 클라이언트로 옮기고 A 객체는 B의 인스턴스를 사용하는 데에만 집중할 수 있도록 한다.


03 의존성 주입

 

생성과 사용을 분리하면 객체는 오로지 인스턴스를 사용하는 책임만 남게 된다. 이것은 외부의 다른 객체가 특정 객체에 인스턴스를 전달해야 한다는 것을 의미한다. 이처럼 사용하는 객체가 아닌 외부의 독립적인 객체가 인스턴스를 생성한 후 이를 전달해서 의존성을 해결하는 방법을 의존성 주입 이라고 부른다. 

 

의존성 주입 종류 3가지

1. 생성자 주입

2. setter 주입

3. 메서드 주입

 

숨겨진 의존성은 나쁘다.

의존성을 주입하는 과정에서 완전하게 숨겨진 의존성은 개발자가 인스턴스 생성에 필요한 모든 인자를 전달하고 있다고 착각 할 수 있다. 하지만 실제로는 모든 의존성을 만족하지 못한 상태에서 런타임 환경에 노출된다면 NullPointException 과 같은 에러상황이 발생할 수 있기 때문에 세부 내용까지는 알 필요가 없지만 객체간 의존관계는 알 수 있도록 하는 것이 좋다.

따라서 명시적인 의존성이 숨겨진 의존성보다는 좋다.

가급적 의존성을 객체의 퍼블릭 인터페이스에 노출하자.


04 의존성 역전 원칙

 

의존성은 변경의 전파와 관련된 것이기 때문에 설계는 변경의 영향을 최소화하도록 의존성을 관리해야 한다.

이 경우 해결방법은 추상화다.

모두가 추상화에 의존하도록 수정하면 하위 수준의 클래스의 변경으로 인해 상위 수준의 클래스가 영향을 받는 것을 방지 할 수 있다. 또한 상위 수준을 재사용할 때 하위 수준의 클래스에 얽매이지 않고도 다양한 컨텍스트에서 재사용이 가능하다.

 

1. 상위 수준의 모듈은 하위 수준의 모듈에 의존해서는 안된다. 둘 다 모두 추상화에 의존해야 한다.(상위 클래스가 구체 객체에 의존해서는 안됨.)

2. 추상화는 구체적인 사항에 의존해서는 안된다. 구체적인 사항은 추상화에 의존해야 한다. 

이를 의존성 역전 원칙 이라고 부른다.

역전이라는 단어 자체를 사용한 이유는 설계의 의존성 방향이 전통적인 절차형 프로그래밍과는 반대 방향으로 나타나기 때문이다. 용어 자체가 반대를 의미하는 것이 아니라 절차지향에서의 흐름에 대한 "역전"을 의미하는 것이다.

 

전통적인 패러다임에서는 상위 수준 모듈이 하위 수준 모듈에 의존했다면 객체지향 패러다임에서는 상위 수준의 모듈과 하위 수준의 모듈이 모두 추상화에 의존한다.


05 유연성에 대한 조언

 

유연하고 재사용 가능한 설계가 항상 좋은 것은 아니다.

유연성은 항상 복잡성을 수반한다. 유연하지 않은 설계는 단순하고 명확하다. 유연한 설계는 복잡하고 암시적이다.

유연성이 높은 코드는 코드상에 표현된 정적인 클래스의 구조와 실행 시점의 동적인 객체 구조가 다르다.

 

불필요한 유연성은 불필요한 복잡성을 낳는다.

변경사항에 대한 가능성을 확인하면서 유연하게 추상화 하는 것이 중요하다.

 

설계를 유연하게 만들기 위해서는 먼저 역할, 책임, 협력에 초점을 맞춰야 한다.

객체의 역할과 책임이 자리를 잡기 전에 너무 성급하게 객체 생성에 집중하는 실수를 하면 안된다.

객체의 생성 메커니즘을 경정하는 시점은 책임 할당의 마지막 단계로 미루고 중요한 비즈니스 로직을 처리하기 위해 책임을 할당하고 협력의 균형을 맞추는 것이 객체 생성에 관한 책임을 할당하는 것 보다 우선이다.

 

책임의 불균형이 심화되고 있는 상태에서 객체의 생성 책임에 대해 집중하는 것은 설계를 하부의 특정한 매커니즘에 종속적으로 만들 확률이 높다. 

이것은 곧 A 가 B 의 특정 구현 클래스에 의존하게 만들 확률이 높다는 것을 의미한다.


Chapter 10 상속과 코드 재사용

01 상속과 중복 코드

 

DRY원칙, 중복 코드는 변경을 방해한다. 이것이 중복 코드를 제거해야 하는 가장 큰 이유다.

반복하지 마라, Don't Repeat Yourself 의 줄인말로 간단히 말해 동일한 지식을 중복하지 말라는 뜻이다.

 

중복 코드는 항상 함께 수정해야 하기 때문에 수정할 때 하나라도 빠뜨린다면 버그로 이어지게 된다.

따라서 중복 코드를 많이 사용하는 것은 언제든 발생할 수 있는 장애 상황에 좀더 높은 확률을 가지고 있는것과 같다.

 

중복 코드를 해결하기 위해서 Enum 타입의 타입 코드를 사용할 때도 있지만 타입 코드를 사용하는 클래스는 낲은 응집도와 높은 결합도라는 문제에 시달리게 된다.

따라서 객체지향 언어는 타입 코드를 사용하지 않고 중복 코드를 관리 할 수 있는 효과적인 방법을 제공 하는데 이 방법이 바로 상속이다.

 

문제는 상속또한 결합도를 높이는데 기여를 한다는 것이다.

상속이 초래하는 부모 클래스와 자식 클래스 사이의 강한 결합도는 코드를 수정하기 어렵게 만든다.

따라서 코드 중복을 제거하기 위해 상속을 사용했음에도 또 다른 로직을 추가하기 위해서 새로운 중복 코드를 만들어야 하는 아이러니한 상황이 발생하게 된다.


02 취약한 기반 클래스 문제

 

상속을 사용하면 부모 클래스의 퍼블릭 인터페이스가 아닌 구현을 변경하더라도 자식 클래스가 영향을 받기 쉬워진다.

부모 클래스의 변경에 의해 자식 클래스가 영향을 받는 현상을 취약한 기반 클래스 문제 라고 한다.

이 문제는 상속을 사용한다면 피할 수 없는 객체지향 프로그래밍의 근복적인 취약점이다.

 

이런 문제를 해결하기 위해서 자식 클래스에서 부모 클래스의 특정 메소드를 사용하지 않는다고 생각할 수도 있지만 인터페이스 설계는 제대로 쓰기엔 쉽게, 엉터리로 쓰기엔 어렵게 만들어야 하기 때문에 특정 상황을 위해서 인터페이스의 제한을 두는 상황을 만든다는 것은 그 어떤 경우에도 정당화하기 어렵다.

 

또 다른 방법으로 내부 구현을 문서화 하라고 말하기도 하지만 객체지향의 핵심은 구현을 캡슐화 하는것이다.

캡슐화 하면서 객체를 감쌌는데 내부 구현을 문서화해서 드러내라는 것은 객체지향이 추구하는 방향과는 맞지 않는다.


03 Phone 다시 살펴보기

 

코드 중복을 제거하기 위해서 상속을 도입할 때 살펴봐야 하는 두 가지 원칙은 다음과 같다.

1. 두 메서드가 유사하게 보인다면 차이점을 메서드로 추출하여 두 메서드를 동일한 형태로 보이도록 만든다.

2. 부모 클래스의 코드를 하위로 내리지 말고 자식 클래스의 코드를 상위로 올린다. 부모 클래스의 구체적인 메서드를 자식 클래스로 내리는 것 보다 자식 클래스의 추상적인 메서드를 부모 클래스로 올리는 것이 재사용성과 응집도 측면에서 더 뛰어난 결과를 얻을 수 있다.

 

상속으로 인한 클래스 사이의 결합을 피할 수 있는 방법은 없다.

상속은 어떤 방식으로든 부모 클래스와 자식 클래스를 결합시킨다.

메서드 구현에 대한 결합은 추상 메서드를 추가 함으로써 어느 정보 완화할 수 있지만 인스턴수 변수에 대한 잠재적인 결합을 제거 할 수는 없다.

원하는 것은 행동을 변경하기 위해 인스턴스 변수를 추가하더라도 상속 계층 전체에 걸쳐 부작용이 퍼지지 않게 막기 위해서는 다른 방법을 사용 해야 한다.


04 차이에 의한 프로그래밍

 

기존 코드와 다른 부분만 추가함으로써 애플리케이션의 기능을 확장하는 방법을 차이에 의한 프로그래밍이라고 한다.

 

상속은 코드 재사용과 관련된 대부분의 경우에 우아한 해결 방법이 아니다.

객체지향에 능숙한 개발자들은 상속의 단점을 피하면서 코드를 재사용 할 수 있는 합성방법을 통해 위와같은 문제를 해결한다.


Chapter 11 합성과 유연한 설계

간단히 말해서 상속은 is-a 관계, 합성은 has-a 관계이다.

상속 관계는 클래스 사이의 정적인 관계인데 비해 합성 관계는 객체 사이의 동적인 관계다. 이것은 코드 작성 시점에 결정한 상속 관계는 변경이 불가능하지만 합성 관계는 실행 시점에 동적으로 변경할 수 있다는 핵심적인 차이가 있다.


01 상속을 합성으로 변경하기

 

기존 상속의 문제점을 세가지로 표현 할 수 있다.

1. 불필요한 인터페이스의 상속 문제

2. 메서드 오버라이딩의 오작용 문제

3. 부모 클래스와 자식 클래스의 동시 수정 문제

 

이러한 문제점들은 상속을 사용한다면 피할 수 없는 문제들이다.


02 상속으로 인한 조합의 폭발적인 증가

 

새로운 기능을 담은 클래스가 필요하다면 새로운 정의를 통한 상속관계를 추가해야 한다. 이러한 것은 꽤 복잡한 과정이기도 하며 그보다 더 큰 문제는 새로운 클래스를 추가하는 것 자체가 어렵다는 것이다. 현재의 설계에 새로운 클래스를 추가하기 위해서는 불필요하게 많은 수의 클래스를 상속 계층 안에 추가해야 하기 때문이다.

 

이러한 상속의 남용으로 하나의 기능을 추가하기 위해서 필요 이상으로 많은 수의 클래스를 추가해야 하는 경우를 가리켜서 클래스 폭발 혹은 조합의 폭발 문제라고 부른다.


03 합성 관계로 변경하기

 

합성은 컴파일타임 관계를 런타임 관계로 변경함으로써 위와 같은 문제를 해결한다.

일반적인 경우 객체의 합성이 클래스 상속보다 더 좋은 방법이다.


04 믹스인

 

상속의 진정한 목적은 자식 클래스를 부모 클래스와 동일한 개념적인 범주로 묶어 is-a 관계를 만들기 위한 것이다. 반면 믹스인은 말 그대로 코드를 다른 코드 안에 섞어 넣기 위한 방법이다.

상속은 정적이지만 믹스인은 동적이다. 상속은 부모 클래스와 자식 클래스의 관계를 코드를 작성하는 시점에 고정시켜 버리지만 믹스인은 제약을 둘뿐 실제로 어떤 코드에 믹스인될 것인지를 결정하지 않는다.

 

믹스인은 this 와 super 참조를 통해서 가리키는 대상이 컴파일 시점이 아닌 실행 시점에 어떠한 메서드를 호출할 것인지 결정 할 수 있다. 이러한 트레이트를 이용하여 선형화(호출 대상의 순서를 정렬함)해서 어떤 메서드를 호출할 지 결정한다.

클래스의 인스턴스를 생성 할 때 클래스 자신과 조상 클래스, 트레이트를 일렬로 나열해서 순서를 정하고 실쟁 중인 메서드 내부에서 super 호출을 하면서 다음 단계에 위치한 클래스나 트레이트의 메서드가 호출된다.

 

결론적으로 순서가 필요한 동작에서는 유용하게 믹스인을 사용 할 수 있다. 트레이트 믹스인이 상속에 비해 코드 재사용과 확장의 관점에서 얼마나 편리한지를 알 수 있으며 믹스인은 재사용 가능한 코드를 독립적으로 작성한 후 필요한 곳에서 쉽게 조립할 수 있도록 해준다.

 

믹스인은 추상 서브클래스 라고 부르기도 한다.