CH9. 유연한 설계

의진·2024년 7월 28일

[Book] 오브젝트

목록 보기
4/4
post-thumbnail

유연하고 재사용 가능한 설계를 만들기 위해 적용할 수 있는 의존성 관리 원칙들을 알아봅시다🤓

📌 개방-폐쇄 원칙

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

✋🏻 확장에 대해 열려있다 → 애플리케이션의 요구사항이 변경될 때 이 변경에 맞게 새로운 '동작'을 추가해서 애플리케이션 기능을 확장할 수 있다
✋🏻 수정에 대해 닫혀 있다 → 기존의 '코드'를 수정하지 않고도 애플리케이션의 동작을 추가하거나 변경할 수 있다


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

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

📖 잠깐! 용어 정리
✔️ 런타임 의존성: 실행 시에 협력에 참여하는 객체들 사이의 관계
✔️ 컴파일타임 의존성: 코드에서 들어나는 클래스들 사이의 관계

🙋🏻‍♀️ 그래서 구체적으로 어떻게 컴파일타임 의존성을 고정시키고 런타임 의존성을 변경하죠?
💡 핵심은 추상화! 추상화란 핵심적인 부분만 남기고, 불필요한 부분은 생략함으로써 복잡성을 극복하는 기법입니다. 추상화 기법을 사용할 때, 변경되지 않을 부분을 신중하게 결정하고 올바른 추상화를 주의 깊게 선택할 때 비로서 추상화가 수정에 대해 닫혀 있을 수 있습니다.


📌 생성 사용 분리

"유연하고 재사용 가능한 설계를 원한다면, 객체에 대한 생성과 사용이라는 두 가지 책임을 서로 다른 객체로 분리해야 한다."

public Movie(String title, Duration runningTime, Money fee) {
	// ...
	this.discountPolicy = new AmountDiscountPolicy(...);
}

객체의 생성에 대한 지식은 과도한 결합도를 초래하는 경향이 있다. 위의 코드는MovieAmountDiscountPolicy를 강하게 결합하도록 만든다!

💡 그럼 Client로 DiscountPolicy에 대한 생성 책임을 넘기자!

public class Client {
	public Money getAvatarFee() {
    	Movie movie = new Movie("아바타",
        						Duration.ofMinues(120),
                                Money.wons(10000),
                                new AmountDiscountPolicy(...));
    }
}

그런데 만약 ClientDiscountPolicy와 의존하지 않았으면 한다면 어떻게 해야 할까?

💡 객체의 생성과 관련된 책임만 전담하는 별도의 객체(FACTORY)를 추가하자!

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

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

어떤 책임을 할당하고 싶다면 제일 먼저 도메인 모델 안에 개념 중에서 적절한 후보가 존재하는지 찾아봐야 한다! 그런데 앞서 살펴 본 FACTORY는 도메인 모델이 아니다! FACTORY는 전체적으로 결합도를 낮추고 재사용성을 높이기 위해 객체 생성 책임을 도메인 개념과 아무 상관없이 없는 가공 객체로 이동시킨 것!

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

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


📌 의존성 주입

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

📖 의존성 주입 방법
✔️ 생성자 주입: 객체를 생성하는 시점에 생성자를 통한 의존성 해결
✔️ setter 주입: 객체 생성 후 setter 메서드를 통한 의존성 해결
✔️ 메서드 주입: 메서드 실행 시 인자를 이용한 의존성 해결

💡 생성자 주입객체의 생명주기 전체에 걸쳐 관계를 유지하는 반면, setter 주입을 사용하면 언제라도 의존 대상을 변경할 수 있다. 하지만 의존성이 필수인지 명시적으로 표시할 수 없고, setter 메서드를 누락한다면 객체가 비정상적인 상태로 생성이 될 수 있다! 메서드 호출 주입은 메서드가 의존성을 필요로 하는 유일한 경우일 때 사용할 수 있다.


💬 가급적 의존성을 객체의 퍼블릭 인터페이스에 노출하라

  • 의존성을 구현 내부로 감출 경우 의존성과 관련된 문제가 컴파일타임이 아닌 런타임에 가서야 발견된다.
  • 의존성을 숨기는 코드는 단위 테스트 작성이 어렵다.
  • 숨겨진 의존성이 캡슐화를 위반한다. 클래스의 퍼블릭 인터페이스만으로 사용 방법을 이해할 수 있는 코드가 캡슐화 관점에서 휼륭한 코드다. 클래스의 사용법을 익히기 위해 구현 내부를 샅샅이 뒤져야 한다면 그 클래스는 캡슐화가 무너진 것이다.
  • 숨겨진 의존성은 의존성의 대상을 설정하는 시점과 의존성이 해결되는 시점을 멀리 떨어트려 놓는다. 이것은 코드를 이해하고 디버깅하기 어렵게 만든다

📌 의존성 역전 원칙

"상위 수준의 모듈은 하위 수준의 모듈(구체적인 사항)에 의존해서는 안된다. 둘 모두 추상화에 의존해야 한다"

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

객체 사이의 협력이 존재할 때 그 협력의 본질을 담고 있는 것은 상위 수준의 정책이다. 즉, 어떤 협력에서 중요한 정책이나 의사결정, 비즈니스의 본질을 담고 있는 것은 상위 수준의 클래스다. 그러나 상위 수준 클래스가 하위 수준의 클래스에 의존한다면 하위 수준의 변경에 의해 상위 수준 클래스가 영향을 받게 될 것 이다.


💬 역전은 의존성의 방향뿐만 아니라 인터페이스의 소유권에도 적용된다

위의 그림은 인터페이스가 서버 모듈 쪽에 위치하는 전통적인 모듈 구조이다. 의존성 역전 원칙을 지켜 설계하였지만, 이 상황에서 Movie를 다양한 컨텍스트에서 재사용하기 위해서는 불필요한 클래스들이 Movie와 함께 배포되어야 한다.

💡 즉, 불필요한 클래스들을 같은 패키지에 두는 것은 전체적인 빌드 시간을 가파르게 상승시킨다.


이렇게 Movie와 추상 클래스인 DiscountPolicy를 하나의 패키지로 모으는 것은 Movie를 특정한 컨텍스트로부터 완벽하게 독립시킨다.


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

유연하고 재사용 가능한 설계가 항상 좋은 것은 아니다. 유연성은 항상 복잡성을 수반한다. 설계가 유연할수록 클래스 구조와 객체 구조 사이의 거리는 점점 멀어진다. 따라서 유연함은 단순성과 명확성의 희생 위에서 자라난다.

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


profile
📖 오브젝트

0개의 댓글