9장: 유연한 설계
Last updated
Last updated
Open-Closed Principle
소프트웨어 개체(클래스, 모듈, 함수 등등)는 확장에 대해 열려 있어야 하고, 수정에 대해서는 닫혀 있어야 한다.
애플리케이션의 요구사항이 변경될 때 이 변경에 맞게 새로운 ‘동작’을 기존의 코드를 수정하지 않고도 추가해서 애플리케이션의 기능을 확장할 수 있어야 한다.
런타임 의존성은 실행 시에 협력에 참여하는 객체들 사이의 관계이고, 컴파일타임 의존성은 코드에서 드러나는 클래스들 사이의 관계다.
개방-폐쇄 원칙을 수용하는 코드는 컴파일타임 의존성을 수정하지 않고도 런타임 의존성을 쉽게 변경 할 수 있다.
추상화에 의존하면 컨텍스트가 바뀌더라도 공통적인 부분은 변하지 않고 공통적이지 않은 부분은 생략한다. 즉, 수정에 대해 닫혀있는 것이므로 추상화에 의존하면 개방-폐쇄 원칙을 지키게 된다.
변경에 의한 파급효과를 최대한 피하기 위해서는 변하는 것과 변하지 않는 것이 무엇인지를 이해하고 이를 추상화의 목적으로 삼아야만 한다.
추상화에만 의존하려면 구체 클래스의 인스턴스를 생성하면 안된다. 왜냐하면 동작을 추가하거나 변경하기 위해서 기존 코드를 수정해야 하기 때문이다.
유연하고 재사용 가능한 설계를 위해서는 객체에 대한 생성과 사용을 분리해야 한다.
가장 보편적인 생성과 사용 분리 방법은 객체를 생성할 책임을 컨텍스트에 대한 결정권을 갖고 있는 클라이언트로 옮기는 것이다.
클라이언트가 객체를 생성하고 객체를 사용하는 경우 객체 생성과 사용의 분리가 되지 않을 수 있다. 만약 이를 분리함으로써 클라이언트도 특정 컨텍스트에 묶이지 않길 바란다면 팩토리 방식을 사용할 수 있다.
팩토리 클래스는 객체 생성의 책임을 가지고, 클라이언트는 객체의 사용에 관련한 책임만 가질 수 있다.
이전에 책임 할당 원칙 중 Information Expert 원칙이 있었는데, 이는 도메인 모델 안의 개념 중 적절한 후보가 있는지 확인하고 책임 수행에 필요한 정보를 가장 많이 아는 객체에 책임을 할당하는 것이다.
팩토리 클래스는 도메인 개념과 아무런 상관이 없지만 객체 생성 책임을 가진다.
시스템을 객체로 분해하는 방법에는 도메인에 존재하는 사물 또는 개념을 표현하는 객체들을 이용해 시스템을 분해하는 표현적 분해와 도메인 개념들을 초월하여 설계자의 편의를 위한 임의의 가공 객체에 책임을 할당하는 행위적 분해로 나뉜다.
Pure Fabrication
책임을 할당하기 위해 창조되는 도메인과 무관한 인공적인 객체이다.
순수히 전체 설계의 품질을 높이기 위해 설계자의 임의에 따라 추가한 가공물인 객체이다.
어떤 행동을 추가하고자 하지만 책임질만한 도메인 개념이 없다면 이 객체에게 책임을 할당하면 된다.
일반적으로 행위적 분해에 의해 생성된다.
대부분의 디자인 패턴은 Pure Fabrication을 포함한다.
외부의 독립적인 객체가 인스턴스를 생성한 후 이를 전달해서 의존성을 해결하는 방법
의존성을 해결하기 위해 의존성을 객체의 퍼블릭 인터페이스에 명시적으로 드러내서 외부에서 필요한 런타임 의존성을 전달할 수 있도록 만드는 방법을 포괄한다.
의존성 주입은 의존성을 명시적으로 표현할 수 있다.
의존성을 해결하는 세 가지 방법은 아래와 같다.
생성자 주입(constructor injection)
객체를 생성하는 시점에 생성자를 통한 의존성 해결
객체의 생명주기 전체에 걸쳐 관계를 유지한다.
setter 주입(setter injection)
객체 생성 후 setter 메서드를 통한 의존성 해결
의존성의 대상을 런타임에 변경할 수 있다.
객체가 올바른 상태가 되기 위해 어떤 의존성이 필수적인지 명시적으로 표현이 불가능하다.
메서드 주입(method injection)
메서드 실행 시 인자를 이용한 의존성 해결
주입된 의존성이 특정 메서드에서만 사용된다면, 메서드 인자로 전달받는 것이 좋다.
인터페이스 주입
주입할 의존성을 명시하기 위해 인터페이스를 사용하는 방법이다.
특정 인터페이스에 의존성을 입력받는 메서드를 선언해두고, 인터페이스의 구현체에서 해당 메서드를 재정의하면서 의존성이 주입된다.
Service Locator 패턴
객체가 Service Locator에게 의존성 해결을 위해 요청을 보내는 방식으로 의존성을 해결할 수 있도록 하는 방법이다.
이 패턴은 의존성을 감추기 때문에 의존성과 관련된 문제가 컴파일 타임이 아닌 런타임에 가서 발견된다. (생성자 주입이었다면 컴파일 에러부터 났을 것이다.)
그리고 의존성을 이해하기 위해 코드의 내부 구현을 이해할 것을 강요하므로 캡슐화를 위반한다. 필요한 의존성은 클래스의 퍼블릭 인터페이스에 명시적으로 드러나야 한다.
의존성을 숨기는 코드는 단위 테스트 작성도 어렵다. 각 단위테스트는 서로 고립되어야 하는 원칙이 있지만 ServiceLocator가 내부적으로 정적 변수를 이용해 객체를 관리하기 때문에 모든 단위 테스트에 거쳐 ServiceLocator의 상태를 공유하게 된다.
깊은 호출 계층에 걸쳐 동일한 객체를 계속해서 전달해야 하는 고통을 견디기 어려운 경우에만 어쩔 수 없이 SERVICE LOCATOR 패턴을 사용하는 것을 고려하도록 한다.
어떤 협력에서 중요한 정책이나 의사결정, 비즈니스의 본질을 담고 있는 것을 상위 수준의 클래스라고 하며, 이러한 상위 수준 클래스가 하위 수준 클래스에 의존하면 하위 수준의 변경에 의해 상위 수준 클래스가 영향을 받을 수 있다.
상위 수준의 클래스가 하위 수준의 클래스에 의존하는 대신, 상위 수준의 클래스와 하위 수준의 클래스가 모두 추상화에 의존해야 한다.
의존성 역전 원칙을 따르는 설계는 의존성의 방향이 전통적인 절차형 프로그래밍과는 반대 방향으로 나타난다.
Seperated Interface 패턴
자바의 모듈을 구현할 수 있는 패키지 단위를 기준으로 추상화 인터페이스가 구현 클래스와 같은 패키지에 존재한다면, 구현 클래스 수정 시 할인 정책 패키지가 재컴파일 되어야 하고, 이에 따라 영화 패키지도 재컴파일되어야 할 수 있다.
아래는 추상화 인터페이스인 DiscountPolicy와 하위 수준 클래스가 함께 패키지에 존재하여, 상위 수준 클래스인 Movie가 하위 수준 클래스의 변경에 영향을 받을 수 있는 구조이다.
이를 방지하기 위해 추상화를 별도의 독립적인 패키지가 아니라 클라이언트가 속한 패키지에 포함시켜야 한다.
의존성 역전 원칙에 따라 상위 수준의 협력 흐름을 재사용하기 위해서는 추상화가 제공하는 인터페이스의 소유권을 서버가 아닌 클라이언트로 역전시켜야 한다.
유연성은 복잡성과 암시성을 수반하며, 유연하지 않은 설계는 단순하고 명확하다.
복잡성에 대한 걱정보다 유연하고 재사용 가능한 설계의 필요성이 더 크다면 코드의 구조와 실행 구조를 다르게 만들어야 한다.
설계를 유연하게 만들기 위해서는 결국 역할, 책임, 협력에 초점을 맞춰야 하며, 이에 대한 설계의 유연성과 재사용성을 위해 의존성 관리가 필요하다는 것을 기억해야 한다.
다양한 컨텍스트에서 협력을 재사용할 수 있도록 설계한다면, 공통의 추상화를 도출하고 동일한 역할을 여러 객체들 간 대체해가며 수행할 수 있도록 할 수 있다.
중요한 비즈니스 로직을 처리하기 위해 책임을 할당하고 협력의 균형을 맞춘 후에 객체를 생성하는 방법을 결정해야 한다.