오브젝트 9장 객체지향 프로그래밍

장현진·2022년 8월 20일
0
post-custom-banner

Ch.9 유연한 설계

개방-폐쇄 원칙(Open-Closed Principle, OCP)

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

  • 확장에 대해 열려 있다 : 애플리케이션의 요구사항이 변경될 때 이 변경에 맞게 새로운 '동작'을 추가해서 애플리케이션의 기능을 확장할 수 있다.
  • 수정에 대해 닫혀 있다 : 기존의 '코드'를 수정하지 않고도 애플리케이션의 동작을 추가하거나 변경할 수 있다.
    • 런타임시점에 기능이 결정된다.
    • 외부주입(의존성)으로 확장성을 부여한다. // 외부주입의 if/else는 어디까지 미루어야하는가? client가 하면 맞는것인가? api새로 를 만들어야 하나?

의존성 관점에서 개방-폐쇄 원칙을 따르는 설계란 컴파일타임 의존성은 유지하면서 런타임 의존성의 가능성을 확장하고 수정할 수 있는 구조이다.

올바른 추상화를 설계하고 추상화에 대해서만 의존하도록 관계를 제한함으로써 설계를 유연하게 확장할 수 있다.
추상화를 했다고 해서 모든 수정에 대해 설계가 폐쇄되는 것은 아니다.
추상화가 수정에 대해 닫혀 있을 수 있는 이유는 변경되지 않을 부분을 신중하게 결정하고 올바른 추상화를 주의 깊게 선택했기 때문이다.

생성 사용분리

사용으로부터 생성을 분리하는 가장 보편적인 방법은 객체를 생성할 책임을 클라이언트로 옮기는것이다.
하지만 이방법은 Client가 movie에대한 너무 많은 정보를 알게 한다. 이를위해 Factory를 적용한다.

FACTORY

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

public class Factory {
	public Movie createAvatarMovie() {
    	return new Movie("아바타", 
        				Duration.ofMinutes(120),
        				Money.wons(100000), 
                        new AmountDiscountPolicy(...));
    }
}
이제 ClientFactory를 사용해서 생성된 Movie의 인스턴스를 반환받아 사용하기만 하면 된다.

public class Client {
	private Factory factory;
    
    public Client(Factory factory) {
    	this.factory = factory;
    }
    
    public Money getAvatarFee() {
    	Movie avatar = factory.createAvatarMovie();
        return avatar.getFee();
    }
}

Factory의 핵심은 도메인 모델에 속하지 않는다는 것이다.


크레이그 라만의 시스템을 객체로 분해하는 두 가지 방식
표현적 분해(representational decomposition)
행위적 분해(behavioral decomposition)
표현적 분해
도메인에 존재하는 사물 또는 개념을 표현하는 객체들을 이용해 시스템을 분해하는 것
(객체지향 설계를 위한 가장 기본적인 접근법)

=> 모든 책임을 도메인 객체에 할당하면 낮은 응집도, 높은 결합도, 재사용성 저하와 같은 심각한 문제점에 봉착하게 될 가능성이 높아진다. 설계자가 편의를 우이해 임의로 만들어낸 가공의 객체에게 책임을 할당해서 문제를 해결해야 한다.

PURE FABRICATION(순수한 가공물) : 책임을 할당하기 위해 창조되는 도메인과 무관한 인공적인 객체

어떤 행동을 책임질 마땅한 도메인 개념이 존재하지 않을 때 PURE FABRICATION을 추가하고 이 객체에게 책임을 할당하라.

PURE FABRICATION은 보통 즉정한 행동을 표현하는 것이 일반적이기 때문에 표현적 분해보다는 행위적 분해에 의해 생성되는 것이 일반적이다.

즉, 도메인의 정보 부하는 디자인 패턴을 이용하여 PURE FABRICATION으로 위임한다.


Dependency Injection (의존성 주입)

사용하는 객체가 아닌 외부의 독립적인 객체(FACTORY)가 인스턴스를 생성한 후 이를 전달해서 의존성을 해결하는 방법

  1. 생성자 주입(constructor injection)
  2. setter 주입(setter injection)
  3. 메서드 주입(method injection)
  4. setter 주입
  • 장점
    의존성의 대상을 런타임에 변경할 수 있다.
    생성자 주입을 통해 설정된 인스턴스는 객체의 생명 주기 전체에 걸쳐 관계를 유지하는 반면, setter 주입을 사용하면 언제라도 의존 대상을 교체할 수 있다.

  • 단점
    객체가 올바로 생성되기 위해 어떤 의존성이 필수적인지를 명시적으로 표현할 수 없다.
    객체가 생성된 후 호출하므로 setter 메서드 호출을 누락한다면 객체는 비정상적인 상태로 생성될 것이다.


SERVICE LOCATOR

필요한 의존성을 직접 ServiceLocator의 메서드를 호출해서 해결한다.

public class Movie {
	...
    private DiscountPolicy discountPolicy;
    
    public Movie(String title, Duration runningTime) {
    	this.title = title;
        this.runningTime = runningTime;
        // 직접 요청 !, static으로 선언 되어 있음
        this.discountPolicy = ServiceLocator.discountPolicy();
    
    }

}

단점 : 의존성을 감춘다

=> 문제점을 발견할 수 있는 시점을 코드 작성 시점이 아니라 실행 시점으로 미룬다.



의존성 역전 원칙

public class Movie {
	private AmountDiscountPolicy discountPolicy;
}

상위 수준 클래스인 Movie가 하위 수준 클래스인 AmountDiscountPolicy에 의존하고 있다.

의존성의 방향은 하위 수준에서 상위 수준으로 흘러야 한다.
Movie를 재사용하기 위해서는 Movie가 의존하는 AmountDiscountPolicy 역시 함께 재사용해야 한다.
상위 수준의 클래스가 하위 수준의 클래스에 의존하면 상위 수준의 클래스를 재사용할 때 하위 수준의 클래스도 필요하기 때문에 재사용하기가 어려워진다.
상위 수준의 변경에 의해 하위 수준이 변경되는 것은 납득할 수 있지만 하위 수준의 변경으로 인해 상위 수준이 변경돼서는 곤란하다.

이 경우에도 해결책은 추상화다.
AmountDiscountPolicy보다 상위 수준인 DiscountPolicy를 의존하게 하여 해결할 수 있다.

상위 수준의 모듈은 하위 수준의 모듈에 의존해서는 안 된다. 둘 모두 추상화에 의존해야 한다.
추상화는 구체적인 사항에 의존해서는 안 된다. 구체적인 사항은 추상화에 의존해야 한다.
이를 의존성 역전 원칙(Dependency Inversion Principle, DIP)라고 한다.

중요한 비즈니스 로직을 처리하기 위해 책임을 할당하고 협력의 균형을 맞추는 것이 객체생성에 관한 책임을 할당하는 것보다 우선이다.
핵심은 객체를 생성하는 방법에 대한 결정은 모든 책임이 자리를 잡은 후 가장 마지막 시점에서 내리는 것이 적절하다.
역할, 책임, 협력의 모습이 선명하게 그려지지 않는다면 의존성을 관리하는 데 들이는 모든 노력이 물거품이 될 수도 있다.

post-custom-banner

0개의 댓글