오브젝트 독서회 8회차 - 9장 유연한 설계

이번 독서회도 @vincent @freebear @콘크리트장인 님이 참여해주셨습니다.


P282

로버트 마틴은 확장 가능하고 변화에 유연하게 대응할 수 있는 설계를 만들 수 있는 원칙 중 하나로 개방-폐쇄 원칙(Open-Closed Principle, OCP) 을 고안했다.[Martin02]. 개방-폐쇄 원칙은 다음과 같은 문장으로 요약할 수 있다.
소프트웨어 개체(클래스, 모듈, 함수 등등)는 확장에 대해 열려 있어야 하고, 수정에 대해서는 닫혀 있어야 한다.
여기서 키워드는 '확장’과 '수정’이다. 이 둘은 순서대로 애플리케이션의 '동작’과 '코드’의 관점을 반영한다.

@vincent @콘크리트장인

P283

개방-폐쇄 원칙은 유연한 설계란 기존의 코드를 수정하지 않고도 애플리케이션의 동작을 확장할 수 있는 설계라고 이야기한다.

@콘크리트장인

컴파일타임 의존성을 고정시키고 런타임 의존성을 변경하라

@vincent

P284

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

@콘크리트장인

개방-폐쇄 원칙의 핵심은 추상화에 의존하는 것 이다.

@콘크리트장인

추상화르 사용하면 생략된 부분을 문맥에 적합한 내용으로 채워넣음으로써 각 문맥에 적합하게 기능을 구체화하고 확장할 수 있다.

@콘크리트장인

P286

앞 장에서 설명한 것처럼 명시적 의존성과 의존성 해결 방법을 통해 컴파일타임 의존성을 런타임 의존성으로 대체함으로써 실행 시에 객체의 행동을 확장할 수 있다. 비록 이런 기법들이 개방-폐쇄 원칙을 따르는 코드를 작성하는 데 중요하지만 핵심은 추상화라는 것을 기억하라. 올바른 추상화를 설계하고 추상화에 대해서만 의존하도록 관계를 제한함으로써 설계를 유연하게 확장할 수 있다.

@freebear @콘크리트장인

여기서 주의할 점은 추상화를 했다고 해서 모든 수정에 대해 설계가 폐쇄되는 것은 아니라는 것이다. 수정에 대해 닫혀 있고 확장에 대해 열려 있는 설계는 공짜로 얻어지지 않는다. 변경에 의한 파급효과를 최대한 피하기 위해서는 변하는 것과 변하지 않는 것이 무엇인지를 이해하고 이를 추상화의 목적으로 삼아야만 한다. 추상화가 수정에 대해 닫혀 있을 수 있는 이유는 변경되지 않을 부분을 신중하게 결정하고 올바른 추상화를 주의 깊게 선택했기 때문이라는 사실을 기억하라.

@freebear @콘크리트장인 @vincent

P287

결합도가 높아질수록 개방-폐쇄 원칙을 따르는 구조를 설계하기가 어려워진다. 알아야 하는 지식이 많으면 결합도도 높아진다. 특히 객체 생성에 대한 지식은 과도한 결합도를 초래하는 경향이 있다.

@vincent

물론 객체 생성을 피할 수는 없다. 어딘가에서는 반드시 객체를 생성해야 한다. 문제는 객체 생성이 아니다. 부적절한 곳에서 객체를 생성한다는 것이 문제다.

@vincent

동일한 클래스 안에서 객체 생성과 사용이라는 두 가지 이질적인 목적을 가진 코드가 공존하는 것이 문제인 것이다.

@vincent

P288

유연하고 재사용 가능한 설계를 원한다면 객체와 관련된 두 가지 책임을 서로 다른 객체로 분리해야 한다. 하나는 객체를 생성하는 것이고, 다른 하나는 객체를 사용하는 것이다.

@콘크리트장인

한 마디로 말해서 객체에 대한 생성과 사용을 분리(separating use from creation) [Bain08]해야 한다.

@vincent

P289

이처럼 생성과 사용을 분리하기 위해 객체 생성에 특화된 객체를 FACTORY라고 부른다[Evans03].

@vincent

P291

눈치가 빠른 사람이라면 방금 전에 추가한 FACTORY는 도메인 모델에 속하지 않는다는 사실을 알아챘을 것이다. FACTORY를 추가한 이유는 순수하게 기술적인 결정이다. 전체적으로 결합도를 낮추고 재사용성을 높이기 위해 도메인 개념에게 할당돼 있던 객체 생성 책임을 도메인 개념과는 아무런 상관이 없는 가공의 객체로 이동시킨 것이다.

@콘크리트장인 @vincent

표현적 분해는 도메인에 존재하는 사물 또는 개념을 표현하는 객체들을 이용해 시스템을 분해하는 것이다. 표현적 분해는 도메인 모델에 담겨 있는 개념과 관계를 따르며 도메인과 소프트웨어 사이의 표현적 차이를 최소화하는 것을 목적으로 한다. 따라서 표현적 분해는 객체지향 설계를 위한 가장 기본적인 접근법이다.
그러나 종종 도메인 개념을 표현하는 객체에게 책임을 할당하는 것만으로는 부족한 경우가 발생한다. 도메인 모델은 설계를 위한 중요한 출발점이지만 단지 출발점이라는 사실을 명심해야 한다. 실제로 동작하는 애플리케이션은 데이터베이스 접근을 위한 객체와 같이 도메인 개념들을 초월하는 기계적인 개념들을 필요로 할 수 있다.

@vincent

p292

이런 측면에서 객체지향이 실세계의 모방이라는 말은 옳지 않다. 객체지향 애플리케이션은 도메인 개념뿐만 아니라 설계자들이 임의적으로 창조한 인공적인 추상화들을 포함하고 있다.

@vincent

설계자로서 우리의 역할은 도메인 추상화를 기반으로 애플리케이션 로직을 설계하는 동시에 품질의 측면에서 균형을 맞추는 데 필요한 객체들을 창조하는 것이다.

@콘크리트장인

먼저 도메인의 본질적인 개념을 표현하는 추상화를 이용해 애플리케이션을 구축하기 시작하라. 만약 도메인 개념이 만족스럽지 못하다면 주저하지 말고 인공적인 객체를 창조하라.

@vincent

P293

14장에서 살펴보겠지만 대부분의 디자인 패턴은 PURE FABRICATION 을 포함한다.

@vincent

이처럼 사용하는 객체가 아닌 외부의 독립적인 객체가 인스턴스를 생성한 후 이를 전달해서 의존성을 해결하는 방법을 의존성 주입(Dependency Injection) [Fowler04]이라고 부른다.

@콘크리트장인

P297

의존성은 암시적이며 코드 깊숙한 곳에 숨겨져 있다.

@콘크리트장인

P298

숨겨진 의존성이 이해하기 어렵고 디버깅하기 어려운 이유는 문제점을 발견할 수 있는 시점을 코드 작성 시점이 아니라 실행 시점으로 미루기 때문이다.

@콘크리트장인

의존성을 숨기는 코드는 단위 테스트 작성도 어렵다.

@콘크리트장인

캡슐화는 코드를 읽고 이해하는 행위와 관련이 있다. 클래스의 퍼블릭 인터페이스만으로 사용 방법을 이해할 수 있는 코드가 캡슐화 관점에서 훌륭한 코드다. 클래스의 사용법을 익히기 위해 구현 내부를 샅샅이 뒤져야 한다면 그 클래스의 캡슐화는 무너진 것이다.

@freebear

따라서 숨겨진 의존성은 캡슐화를 위반한다.

@콘크리트장인

P299

이야기의 핵심은 의존성 주입이 SERVICE LOCATOR 패턴보다 좋다가 아니라 명시적인 의존성이 숨겨진 의존성보다 좋다는 것이다.

@콘크리트장인

가급적 의존성을 객체의 퍼블릭 인터페이스에 노출하라. 의존성을 구현 내부에 숨기면 숨길수록 코드를 이해하기도, 수정하기도 어려워진다.

@freebear

접근해야 할 객체가 있다면 전역 메커니즘 대신, 필요한 객체를 인수로 넘겨줄 수는 없는지부터 생각해보자. 이 방법은 굉장히 쉬운 데다 결합을 명확하게 보여줄 수 있다. 대부분은 이렇게만 해도 충분하다.

@콘크리트장인 @vincent

P301

상위 수준의 클래스는 어떤 식으로든 하위 수준의 클래스에 의존해서는 안 되는 것이다.

@freebear

중요한 것은 상위 수준의 클래스다. 상위 수준의 변경에 의해 하위 수준이 변경되는 것은 납득할 수 있지만 하위 수준의 변경으로 인해 상위 수준이 변경돼서는 곤란하다.

@콘크리트장인 @vincent

가장 중요한 조언은 추상화에 의존하라는 것이다. 유연하고 재사용 가능한 설계를 원한다면 모든 의존성의 방향이 추상 클래스나 인터페이스와 같은 추상화를 따라야 한다. 구체 클래스는 의존성의 시작점이어야 한다. 의존성의 목적지가 돼서는 안 된다.

@freebear

P302

이를 의존성 역전 원칙(Dependency Inversion Principle, DIP) [Martin02]이라고 부른다. 이 용어를 최초로 착안한 로버트 마틴은 '역전(inversion)'이라는 단어를 사용한 이유에 대해 의존성 역전 원칙을 따르는 설계는 의존성의 방향이 전통적인 절차형 프로그래밍과는 반대 방향으로 나타나기 떄문이라고 설명한다.

@vincent

잘 설계된 객체지향 프로그램의 의존성 구조는 전통적인 절차적 방법에 의해 일반적으로 만들어진 의존성 구조에 대해 '역전’된 것이다[Martin02].

@콘크리트장인 @vincent

P304

정리하자. 유연하고 재사용 가능하며 컨텍스트에 독립적인 설계는 전통적인 패러다임이 고수하는 의존성의 방향을 역전시킨다. 전통적인 패러다임에서는 상위 수준의 모듈이 하위 수준 모듈에 의존했다면 객체지향 패러다임에서는 상위 수준 모듈과 하위 수준 모듈이 모두 추상화에 의존한다. 전통적인 패러다임에서는 인터페이스가 하위 수준 모듈에 속했다면 객체지향 패러다임에서는 인터페이스가 상위 수준 모듈에 속한다.

@콘크리트장인

P305

변경은 예상이 아니라 현실이어야 한다. 미래에 변경이 일어날지도 모른다는 막연한 불안감은 불필요하게 복잡한 설계를 낳는다. 아직 일어나지 않은 변경은 변경이 아니다.

@vincent

유연성은 항상 복잡성을 수반한다.

@콘크리트장인

설계가 유연할수록 클래스 구조와 객체 구조 사이의 거리는 점점 멀어진다.

@콘크리트장인 @vincent

P306

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

@콘크리트장인

내 두 번째 주장은 우리의 지적 능력은 정적인 관계에 더 잘 들어맞고, 시간에 따른 진행 과정을 시각화하는 능력은 상대적으로 덜 발달했다는 점이다. 이러한 이유로 우리는 (자신의 한계를 알고 있는 현명한 프로그래머로서) 정적인 프로그램과 동적인 프로세스 사이의 간극을 줄이기 위해 최선을 다해야 하며, 이를 통해 프로그램(텍스트 공간에 흩뿌려진)과 (시간에 흩뿌려진) 진행 과정 사이를 가능한 한 일치시켜야 한다[Dijkstra68].

@vincent

초보자가 자주 저지르는 실수 중 하나는 객체의 역할과 책임이 자리를 잡기 전에 너무 성급하게 객체 생성에 집중하는 것이다. 이것은 객체 생성과 관련된 불필요한 세부사하에 객체를 결합시킨다. 객체를 생성할 책임을 담당할 객체나 객체 생성 메커니즘을 결정하는 시점은 책임 할당의 마지막 단계로 미뤄져야만 한다. 중요한 비즈니스 로직을 처리하기 위해 책임을 할당하고 협력의 균형을 맞추는 것이 객체 생성에 관한 책임을 할당하는 것보다 우선이다. 책임 관점에서 객체들 간에 균형이 잡혀 있는 상태라면 생성과 관련된 책임을 지게 될 객체를 선택하는 것은 간단한 작업이 된다.

@freebear @콘크리트장인

P307

불필요한 SINGLETON 패턴[GOF94]은 객체 생성에 관해 너무 이른 시기에 고민하고 결정할 때 도입되는 경향이 있다. 핵심은 객체를 생성하는 방법에 대한 결정은 모든 책임이 자리를 잡은 후 가장 마지막 시점에 내리는 것이 적절하다는 것이다.

@vincent

의존성을 관리해야 하는 이유는 역할, 책임, 협력의 관점에서 설계가 유연하고 재사용 가능해야 하기 때문이다. 따라서 역할, 책임, 협력에 먼저 집중하라. 이번 장에서 설명한 다양한 기법들을 적용하기 전에 역할, 책임, 협력의 모습이 선명하게 그려지지 않는다면 의존성을 관리하는 데 들이는 모든 노력이 물거품이 될 수도 있다는 사실을 명심하라.

@freebear @콘크리트장인

4 Likes

이번 장에서는 제 기억으로는 OOP가 현실세계와 비슷하기도, 다르다기도 하다는 관점을 심어준 첫 장이었습니다.

Service Locator, Factory가 언급되면서 OOP 프로그래밍이 비즈니스 로직을 코드로 구현할 뿐아니라 코드를 유지보수하기 위한 변경과 코드 관점에서의 효율적인 프로그래밍이 등장합니다.

내가 누군가에게 객체에 대해 설명해주면서 데이터의 구성, 집합이라고 설명을 해왔었지만, 꼭 그렇지는 않다라는 점도 생각하게 되었습니다.

그러고 보니 언젠가 MS 직원분이셨는지 기억은 안나지만, IoC Container는 컴파일 타임부터 불완전한 프로그래밍이라며 유명한 개발자가 비판을 했던 것을 본 기억이 있는데, 그런 분은 높은 경지에까지 오르고 나서 왜 IoC Container가 유지보수에 불필요하다고 결론을 내리셨을지 궁금하기도 하네요.

이 책에서는 끊임없이 인터페이스와 비즈니스를 분리하여, 비즈니스는 비즈니스대로 만들고 개발은 메시지인 인터페이스를 통해 개발해서 런타임에서 비즈니스 객체가 인터페이스에 위치하게끔 만들라고 말합니다. 왠지 그 IoC Container를 비판하신 분의 의견이 OOP랑도 좀 일치하지 않는 것 같다고 생각이 드네요. 객체가 아니라 클래스를 지향하는게 이런 것인지…궁금해졌습니다.

3 Likes