[오브젝트] 9장. 유연한 설계

조재훈·2024년 8월 12일

9장. 유연한 설계

01. 개방-폐쇄 원칙

확장 가능하고 변화에 유연하게 대응할 수 있는 설계를 만들 수 있는 원칙 중 하나인 개방-폐쇄 원칙

소프트웨어 개체는 확장에 대해 열려 있어야 하고, 수정에 대해서는 닫혀 있어야 한다

  • 확장에 대해 열려 있다라는 말은 기존 애플리케이션에 새로운 '동작'을 추가해 기능을 확장할 수 있다는 뜻
  • 수정에 대해 닫혀 있다라는 말은 기존의 코드들을 수정하지 않고 기능을 확장할 수 있다는 뜻

즉, 애플리케이션에 새로운 기능을 추가할 때 기존의 코드들을 수정하지 않는 설계로 볼 수 있음

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

컴파일타임 의존성은 우리가 코드를 작성할 때 클래스들 사이 의존성이고 런타임 의존성은 프로그램이 실행될 때 객체들 사이의 관계임

편하게 생각해보면 A 클래스를 상속 받은 B, C 클래스가 있을 때 코드를 작성할 때는 A 클래스로 인스턴스를 보관할 수 있지만 런타임에서는 B, C 클래스만의 기능을 할 수 있다라는 뜻

우리는 이 컴파일타임 의존성과 런타임 의존성을 분리해서 개방-폐쇄 원칙을 따라야 한다

추상화가 핵심이다

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

추상화란 앞에서 설명했듯이 진짜 알고 싶은 정보(핵심, 본질)만 남기는 것이다. 여러 브랜드에서 나오는 자동차들마다 설계 방법이 다 틀리겠지만 겉보기에는 다 똑같은 자동차듯이

개방-폐쇄 원칙의 관점에서 생략되지 않고 남겨진 부분은 추상화의 결과물이다. 공통적인 부분은 문맥이 바뀌더라도 변하지 않아야 한다. 즉, 수정할 필요가 없어야 한다

앞 장에서 설명한 것처럼 명시적 의존성과 의존성 해결 방법을 통해 컴파일타임 의존성을 런타임 의존성으로 대체함으로써 객체의 행동을 확장할 수 있다

올바른 추상화를 설계하고 추상화에 대해서만 의존하도록 관계를 제한해 설계를 유연하게 확장할 수 있다

중요한 점은 추상화를 했다고 해서 모든 수정에 대해 설계가 폐쇄되는 것은 아니며 변경에 의한 파급효과를 최대한 피하기 위해 변하는 것과 변하지 않는 것이 무엇인지를 이해하고 이를 추상화의 목적으로 삼아야 한다

02. 생성 사용 분리

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

객체 생성은 피할 수 없기 때문에 어디서 객체를 생성한다는 것이 중요함. 동일한 클래스에서 객체 생성과 사용이라는 두 가지 이질적인 목적을 가진 코드가 공존하는 것이 문제이다

객체에 대한 생성과 사용을 분리해야 한다. 사용으로부터 생성을 분리하는 데 사용되는 가장 보편적인 방법은 객체를 생성할 책임을 클라이언트로 옮기는 것임

FACTORY 추가하기

객체 생성과 관련된 책임만 전담하는 별도의 객체를 추가하고 클라이언트가 이 객체를 사용하도록 만들어 보자. 생성과 사용을 분리하기 위해 객체 생성에 특화된 객체를 FACTORY라고 부른다

순수한 가공물에게 책임 할당하기

책임 할당의 기본 원칙은 책임을 수행하는 데 필요한 정보를 가장 많이 알고 있는 정보 전문가에게 책임을 할당하는 것. 도메인 모델은 정보 전문가를 찾기 위해 참조할 수 있는 일차적인 재료임. 정보 전문가를 찾으려면 도메인 모델에서 적절한 후보를 찾아봐야 함

크레이그 라만은 시스템을 객체로 분해하는 데에는 크게 두 가지 방식이 존재한다고 설명함

  • 표현적 분해
    • 도메인에 존재하는 사물 또는 개념을 표현하는 객체들을 이용해 시스템을 분해하는 것
    • 도메인과 소프트웨어 사이의 표현적 차이를 최소화하는 것이 목적임
  • 행위적 분해

모든 책임을 도메인 객체에게 할당하면 낮은 응집도, 높은 결합도, 재사용성 저하와 같은 문제점이 생긴다. 이 경우 설계자가 편의를 위해 임의로 만들어낸 가공의 객체에게 책임을 할당해서 문제를 해결해야 한다

크레이그 라만은 책임을 할당하기 위해 창조되는 도메인과 무관한 인공적인 객체를 PURE FABRICATION(순수한 가공물) 이라고 부른다. 이는 특정한 행동을 표현하는 것이 일반적이며 표현적 분해보다는 행위적 분해에 의해 생성되는 것이 일반적이다

이런 측면이 객체지향이 실세계의 모방이라는 말은 옳지 않다. 객체지향 애플리케이션의 대부분은 실제 도메인에서 발견할 수 없는 순수한 인공물로 가득 차 있다

설계자로서 우리의 역할은 도메인 추상화를 기반으로 애플리케이션 로직을 설계하는 동시에 품질의 측면에서 균형을 맞추는 데 필요한 객체들을 창조하는 것임. 도메인 개념을 표현하는 객체와 순수하게 창조된 가공의 객체들이 모여 협력하는 애플리케이션을 설계하는 것이 목표여야 한다

도메인 개념이 만족스럽지 못하면 주저하지 말고 인공적인 객체를 창조하자

03. 의존성 주입

사용하는 객체가 아닌 외부의 독립적인 객체가 인스턴스를 생성한 후 이를 전달해서 의존성을 해결하는 방법을 의존성 주입이라고 부른다

의존성 주입은 의존성을 해결하기 위해 의존성을 객체의 퍼블릭 인터페이스에 명시적으로 드러내 외부에서 필요한 런타임 의존성을 전달할 수 있도록 만드는 방법을 포괄하는 명칭임

  • 생성자 주입 : 객체를 생성하는 시점에 생성자를 통한 의존성 해결
  • setter 주입 : 객체 생성 후 setter 메서드를 통한 의존성 해결
    • 장점 : 의존성의 대상을 런타임에 변경할 수 있다
    • 단점 : 객체가 올바르게 생성되기 위해 어떤 의존성이 필수적인지를 명시적으로 표현할 수 없다는 것
  • 메서드 주입 : 메서드 실행 시 인자를 이용한 의존성 해결
    • 의존성이 한 두개의 메서드에서만 사용된다면 각 메서드의 인자로 전달하는 것이 더 나은 방법일 수 있다

숨겨진 의존성은 나쁘다

의존성 주입 외에도 의존성을 해결하는 방법은 다양하다. 대표적으로 SERVICE LOCATOR 패턴이다. 이는 의존성을 해결할 객체들을 보관하는 일종의 저장소임

객체가 직접 SERVICE LOCATOR에게 의존성을 해결해줄 것을 요청함

의존성을 해결하기 위해 가장 쉽고 간단한 도구인 것처럼 보이지만 저자는 이 패턴을 선호하지 않는다. 가장 큰 단점인 의존성을 감추기 때문이다

의존성을 구현 내부로 감출 경우 의존성과 관련된 문제가 컴파일타임이 아닌 런타임에 가서야 발견된다는 사실을 알 수 있다. 이렇게 되면 런타임에 오류가 발생한다는 것을 알게 됨

의존성을 숨길 경우 단위 테스트 작성도 어렵다. 모든 단위 테스트 케이스에 걸쳐 ServiceLocator의 상태를 공유하게 되어 각 단위 테스트는 서로 고립돼야 한다는 단위 테스트의 기본 원칙을 위반한 것임

숨겨진 의존성이 가지는 가장 큰 문제는 의존성을 이해하기 위해 코드의 내부 구현을 이해할 것을 강요한다는 것임. 따라서 캡슐화를 위반하게 됨

필요한 의존성은 퍼블릭 인터페이스에 명시적으로 드러내야 한다. 그렇게 되면 내부 구현을 읽을 필요도 없고 관련 문제도 컴파일타임에 잡을 수 있다

의존성 주입을 지원하는 프레임워크를 사용하지 못하는 경우나 깊은 호출 계층에 걸쳐 동일한 객체를 계속 전달해야 하는 고통을 견디기 어려운 경우에는 SERVICE LOCATOR 패턴을 사용하는 것을 고려하라

04. 의존성 역전 원칙

추상화와 의존성 역전

객체 사이 협력이 존재할 때 그 협력의 본질을 담고 있는 것은 상위 수준의 정책이다. 어떤 협력에서 중요한 정책이나 의사결정, 비즈니스의 본질을 담고 있는 것은 상위 수준 클래스다

상위 수준 클래스가 하위 수준 클래스에 의존한다면 하위 수준의 변경에 의해 상위 수준 클래스가 영향을 받게 된다. 의존성의 방향이 잘못 되었다

상위 수준의 변경에 의해 하위 수준이 변경되는 것은 납득할 수 있지만 그 반대는 곤란하다

이 경우에도 해결사는 추상화이며 추상화에 의존하면 영향을 받는 것을 방지할 수 있으며 재사용이 가능해 진다

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

정리해보자면
1. 상위 수준 모듈은 하위 수준 모듈에 의존해서는 안 되며 둘 모두 추상화에 의존해야 한다
2. 추상화는 구체적인 사항에 의존해서는 안 된다. 구체적인 사항은 추상화에 의존해야 한다

이를 의존성 역전 원칙이라고 부르며 이렇게 부르는 이유는 의존성의 방향이 전통적인 절차형 프로그래밍과는 반대 방향으로 나타나기 때문이라고 한다

05. 유연성에 대한 조언

유연한 설계는 유연성이 필요할 때만 옳다

항상 유연하고 재사용 가능한 설계가 좋은 것은 아니다. 단순하고 명확한 설계를 가진 코드는 읽기 쉽고 이해하기 편하지만 유연한 설계 즉, 변경하기 쉽고 확장하기 쉬운 구조를 만들기 위해서는 단순함과 명확함의 미덕을 버리게 될 가능성이 높다

유연성은 항상 복잡성을 수반한다. 유연한 설계는 복잡하고 암시적이다. 객체지향 입문 개발자들이 어려워하는 부분이 바로 코드 상에 표현된 정적인 클래스의 구조와 실행 시점의 동적인 객체 구조가 다르다는 사실임

설계가 유연할수록 클래스 구조와 객체 구조 사이의 거리는 점점 멀어진다. 유연한 설계를 단순하게 명확하게 만드는 유일한 방법은 사람들 간 긴밀한 커뮤니케이션뿐임

복잡성에 대한 걱정보다 유연하고 재사용 가능한 설계의 필요성이 더 크다면 코드의 구조와 실행 구조를 다르게 만들어라

협력과 책임이 중요하다

설계를 유연하게 만들기 위해서는 역할, 책임, 협력에 초점을 맞춰야 한다. 다양한 컨텍스트에서 재사용할 일이 없으면 설계를 유연하게 만들 필요가 없다. 객체들이 메시지 전송자의 관점에서 동일한 책임을 수행하는지 여부를 판단할 수 없다면 공통의 추상화를 도출할 수 없다

초보자가 자주 저지르는 실수는 너무 성급하게 객체의 생성에 집중하는 것이다. 이는 객체 생성과 관련된 불필요한 세부사항에 객체를 결합시킨다. 객체를 생성할 책임을 담당할 객체나 객체 생성 매커니즘을 결정하는 시점은 책임 할당의 마지막 단계로 미뤄야 한다

책임의 불균형이 심화되고 있는 상태에서 객체의 생성 책임을 지우는 것은 설계를 하부의 특정한 매커니즘에 종속적으로 만들 확률이 높다. 불필요한 SINGLETON 패턴은 객체 생성에 관해 너무 이른 시기에 고민하고 결정할 때 도입되는 경향이 있다

profile
나태지옥

0개의 댓글