9장: 유연한 설계

개방-폐쇄 원칙

  • Open-Closed Principle

  • 소프트웨어 개체(클래스, 모듈, 함수 등등)는 확장에 대해 열려 있어야 하고, 수정에 대해서는 닫혀 있어야 한다.

  • 애플리케이션의 요구사항이 변경될 때 이 변경에 맞게 새로운 ‘동작’을 기존의 코드를 수정하지 않고도 추가해서 애플리케이션의 기능을 확장할 수 있어야 한다.

  • 런타임 의존성은 실행 시에 협력에 참여하는 객체들 사이의 관계이고, 컴파일타임 의존성은 코드에서 드러나는 클래스들 사이의 관계다.

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

  • 추상화에 의존하면 컨텍스트가 바뀌더라도 공통적인 부분은 변하지 않고 공통적이지 않은 부분은 생략한다. 즉, 수정에 대해 닫혀있는 것이므로 추상화에 의존하면 개방-폐쇄 원칙을 지키게 된다.

  • 변경에 의한 파급효과를 최대한 피하기 위해서는 변하는 것과 변하지 않는 것이 무엇인지를 이해하고 이를 추상화의 목적으로 삼아야만 한다.

객체 생성과 사용 분리

  • 추상화에만 의존하려면 구체 클래스의 인스턴스를 생성하면 안된다. 왜냐하면 동작을 추가하거나 변경하기 위해서 기존 코드를 수정해야 하기 때문이다.

  • 유연하고 재사용 가능한 설계를 위해서는 객체에 대한 생성과 사용을 분리해야 한다.

  • 가장 보편적인 생성과 사용 분리 방법은 객체를 생성할 책임을 컨텍스트에 대한 결정권을 갖고 있는 클라이언트로 옮기는 것이다.

  • 클라이언트가 객체를 생성하고 객체를 사용하는 경우 객체 생성과 사용의 분리가 되지 않을 수 있다. 만약 이를 분리함으로써 클라이언트도 특정 컨텍스트에 묶이지 않길 바란다면 팩토리 방식을 사용할 수 있다.

  • 팩토리 클래스는 객체 생성의 책임을 가지고, 클라이언트는 객체의 사용에 관련한 책임만 가질 수 있다.

  • 이전에 책임 할당 원칙 중 Information Expert 원칙이 있었는데, 이는 도메인 모델 안의 개념 중 적절한 후보가 있는지 확인하고 책임 수행에 필요한 정보를 가장 많이 아는 객체에 책임을 할당하는 것이다.

  • 팩토리 클래스는 도메인 개념과 아무런 상관이 없지만 객체 생성 책임을 가진다.

  • 시스템을 객체로 분해하는 방법에는 도메인에 존재하는 사물 또는 개념을 표현하는 객체들을 이용해 시스템을 분해하는 표현적 분해와 도메인 개념들을 초월하여 설계자의 편의를 위한 임의의 가공 객체에 책임을 할당하는 행위적 분해로 나뉜다.

  • Pure Fabrication

    • 책임을 할당하기 위해 창조되는 도메인과 무관한 인공적인 객체이다.

    • 순수히 전체 설계의 품질을 높이기 위해 설계자의 임의에 따라 추가한 가공물인 객체이다.

    • 어떤 행동을 추가하고자 하지만 책임질만한 도메인 개념이 없다면 이 객체에게 책임을 할당하면 된다.

    • 일반적으로 행위적 분해에 의해 생성된다.

    • 대부분의 디자인 패턴은 Pure Fabrication을 포함한다.

의존성 주입

  • 외부의 독립적인 객체가 인스턴스를 생성한 후 이를 전달해서 의존성을 해결하는 방법

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

  • 의존성 주입은 의존성을 명시적으로 표현할 수 있다.

  • 의존성을 해결하는 세 가지 방법은 아래와 같다.

    • 생성자 주입(constructor injection)

      • 객체를 생성하는 시점에 생성자를 통한 의존성 해결

      • 객체의 생명주기 전체에 걸쳐 관계를 유지한다.

    • setter 주입(setter injection)

      • 객체 생성 후 setter 메서드를 통한 의존성 해결

      • 의존성의 대상을 런타임에 변경할 수 있다.

      • 객체가 올바른 상태가 되기 위해 어떤 의존성이 필수적인지 명시적으로 표현이 불가능하다.

    • 메서드 주입(method injection)

      • 메서드 실행 시 인자를 이용한 의존성 해결

      • 주입된 의존성이 특정 메서드에서만 사용된다면, 메서드 인자로 전달받는 것이 좋다.

    인터페이스 주입

    • 주입할 의존성을 명시하기 위해 인터페이스를 사용하는 방법이다.

    • 특정 인터페이스에 의존성을 입력받는 메서드를 선언해두고, 인터페이스의 구현체에서 해당 메서드를 재정의하면서 의존성이 주입된다.

  • Service Locator 패턴

    • 객체가 Service Locator에게 의존성 해결을 위해 요청을 보내는 방식으로 의존성을 해결할 수 있도록 하는 방법이다.

    public class Movie {
    	
    	private DiscountPolicy discountPolicy;
    	
    	public Movie(String title, Duration runningTime, Money fee) { 
    		this.title = title;
    		this.runningTime = runningTime;
    		this.fee = fee;
    		this.discountPolicy = ServiceLocator.discountPolicy();
    	}
    }
    
    public class ServiceLocator {
    	private static ServiceLocator soleInstance = new ServiceLocator();
    	private DiscountPolicy discountPolicy;
    	
    	public static DiscountPolicy discountPolicy() {
    		return soleInstance.discountPolicy;
    	}
    	
    	public static void provide(DiscountPolicy discountPolicy) {
    		soleInstance.discountPolicy = discountPolicy;
    	}
    }
    
    public static void main(String[] args) {
    	// Movie 객체 생성
    	ServiceLocator.provide(new AmountDiscountPolicy(...)); 
    	Movie avatar = new Movie("아바타", Duration.ofMinutes(120), Money.wons(10000));
    }
    • 이 패턴은 의존성을 감추기 때문에 의존성과 관련된 문제가 컴파일 타임이 아닌 런타임에 가서 발견된다. (생성자 주입이었다면 컴파일 에러부터 났을 것이다.)

    • 그리고 의존성을 이해하기 위해 코드의 내부 구현을 이해할 것을 강요하므로 캡슐화를 위반한다. 필요한 의존성은 클래스의 퍼블릭 인터페이스에 명시적으로 드러나야 한다.

    • 의존성을 숨기는 코드는 단위 테스트 작성도 어렵다. 각 단위테스트는 서로 고립되어야 하는 원칙이 있지만 ServiceLocator가 내부적으로 정적 변수를 이용해 객체를 관리하기 때문에 모든 단위 테스트에 거쳐 ServiceLocator의 상태를 공유하게 된다.

    • 깊은 호출 계층에 걸쳐 동일한 객체를 계속해서 전달해야 하는 고통을 견디기 어려운 경우에만 어쩔 수 없이 SERVICE LOCATOR 패턴을 사용하는 것을 고려하도록 한다.

의존성 역전 원칙

  • 어떤 협력에서 중요한 정책이나 의사결정, 비즈니스의 본질을 담고 있는 것을 상위 수준의 클래스라고 하며, 이러한 상위 수준 클래스가 하위 수준 클래스에 의존하면 하위 수준의 변경에 의해 상위 수준 클래스가 영향을 받을 수 있다.

  • 상위 수준의 클래스가 하위 수준의 클래스에 의존하는 대신, 상위 수준의 클래스와 하위 수준의 클래스가 모두 추상화에 의존해야 한다.

  • 의존성 역전 원칙을 따르는 설계는 의존성의 방향이 전통적인 절차형 프로그래밍과는 반대 방향으로 나타난다.

  • Seperated Interface 패턴

    • 자바의 모듈을 구현할 수 있는 패키지 단위를 기준으로 추상화 인터페이스가 구현 클래스와 같은 패키지에 존재한다면, 구현 클래스 수정 시 할인 정책 패키지가 재컴파일 되어야 하고, 이에 따라 영화 패키지도 재컴파일되어야 할 수 있다.

    • 아래는 추상화 인터페이스인 DiscountPolicy와 하위 수준 클래스가 함께 패키지에 존재하여, 상위 수준 클래스인 Movie가 하위 수준 클래스의 변경에 영향을 받을 수 있는 구조이다.

    • 이를 방지하기 위해 추상화를 별도의 독립적인 패키지가 아니라 클라이언트가 속한 패키지에 포함시켜야 한다.

    • 의존성 역전 원칙에 따라 상위 수준의 협력 흐름을 재사용하기 위해서는 추상화가 제공하는 인터페이스의 소유권을 서버가 아닌 클라이언트로 역전시켜야 한다.

유연성

  • 유연성은 복잡성과 암시성을 수반하며, 유연하지 않은 설계는 단순하고 명확하다.

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

  • 설계를 유연하게 만들기 위해서는 결국 역할, 책임, 협력에 초점을 맞춰야 하며, 이에 대한 설계의 유연성과 재사용성을 위해 의존성 관리가 필요하다는 것을 기억해야 한다.

  • 다양한 컨텍스트에서 협력을 재사용할 수 있도록 설계한다면, 공통의 추상화를 도출하고 동일한 역할을 여러 객체들 간 대체해가며 수행할 수 있도록 할 수 있다.

  • 중요한 비즈니스 로직을 처리하기 위해 책임을 할당하고 협력의 균형을 맞춘 후에 객체를 생성하는 방법을 결정해야 한다.

Last updated