ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [오브젝트 - 4 ] 개방 폐쇄 원칙과 의존성
    개발/프로그래밍 2022. 6. 11. 16:01

    new연산자

    new연산자는 인스턴스를 생성할때 사용한다.

    그러나 인스턴스 생성시 추상클래스가 아닌 구체클래스를 직접 결합해 사용(의존)하며 생성시 필요한 인자값도 넣어줘야하므로 클라이언트 입장에서 지식양이 늘어난다.

    class Movie {
    	private let discountPolicy:DiscountPolicy
    	init(title:String, runningTime:Duration, fee:Movie) {
    		self.discountPlicy = AmountDiscountPolicy(Money.wons(800), SequenceCondition(1),PeriodCondition(DayOfWeek.Monday),PeriodCondition(DayOfWeek.Thursday))
    	}
    }
    

    위와같이 Movie클래스가 알아야할 지식을 늘리고 AmountDiscountPolicy 에대한 강한 결합을 하게된다.

    또 SequenceCondition , PeriodCondition에 도 결합되다.

    해결을 위해서는 인스턴스 생성로직과 사용로직을 분리하는것이다.

    즉 인스턴스 생성은 외부에서하고 전달받아 사용은 내부에서하는것이다.

    class Movie {
    	private let discountPolicy:DiscountPolicy
    	init(title:String, runningTime:Duration, fee:Movie,discountPolicy:DiscountPolicy) {
    		self.discountPlicy = discountPolicy
    	}
    }
    

    movie 생성자는 이제 구체클래스인 AmountDiscountPolicy 가 아닌

    추상 클래스인 DiscountPolicy에 의존한다. 이제 더 유연해졌다.

    하지만 만약 Movie 클래스들이 몇개는 AmountDiscountPolicy 를 사용하지만 몇개는PercentDiscountPolicy 를 사용한다면

    클라이언트에서 중복코드가 늘고 사용성이 떨어지게된다.

    Movie("avatar",Duration(120),Money.wons(10000),AmountDiscountPolicy(Money.wons(800), SequenceCondition(1),PeriodCondition(DayOfWeek.Monday),PeriodCondition(DayOfWeek.Thursday))
    Movie("avatar2",Duration(130),Money.wons(10000),PercentDiscountPolicy(Money.wons(800), SequenceCondition(10),PeriodCondition(DayOfWeek.Monday),PeriodCondition(DayOfWeek.Thursday))
    

    매번 다른 policy가 적용되는게 아니라면

    convenience init 함수사용 할수있다

    class Movie {
    	private let discountPolicy:DiscountPolicy
    	
    	conveinience init(title:String, runningTime:Duration, fee:Movie,discountPolicy:DiscountPolicy) {
    		self.init(title,runningTime,fee,AmountDiscountPolicy(...))
    	}
    
    	init(title:String, runningTime:Duration, fee:Movie,discountPolicy:DiscountPolicy) {
    		self.discountPlicy = discountPolicy
    	}
    }
    

    표준클래스

    표준클래스란 String ,Array 같은 기본으로 제공되는 class 를 말한다

    이들은 변경 가능성이 0이므로 의존해도 상관이 없다.

    예외 케이스

    두가지 케이스를 더 추가해보자. 1번은 할인혜택이 없는영화의 경우, 2번은 다수의 할인혜택을 적용하느 경우이다.

    만약 할인 혜택이 없는경우는 discountPolicy 를 null 체크를 하여서

    할인 계산을 거르는 코드를 작성할것이다.

    그러나 예외상황에서 코드 내부를 직접 수정하는것은 좋은방법이 아니다.

    차라리 NoneDiscountPolicy 클래스를 만들어 사용하는것이 바람직 하다.

    다수의 할인혜택의 경우 discountPolicy를 배열형태로 받아주는 방법이 있다. 그러나 역시 코드 내부를 수정해야한다.

    역시 OverlappedDiscountPolicy 클래스를 만들어서 사용하느것이 바람직 하다.

    Movie("avatar",
           Duration(120),
           Money.wons(10000),
    	   OverlappedDiscountPolicy(AmountDiscountPolicy(...),AmountDiscountPolicy(...),PercentDiscountPolicy(...)
    )

    개방 폐쇄 원칙

    개방폐쇄원칙은 확장에 열려있고 수정에 닫혀있어야한다는 뜻이다.

    이의미는 풀어서 어플리케이션의 요구사항이 추가될때

    새로운 동작을 추가해 기능을 확장하고

    기존 코드를 수정하지 않고 기능을 확장하라는 말이다.

    컴파일타임 의존성과 런타임의존성

    위 Movie 에서 컴파일타임에서 Movie는 DiscountPolicy(추상) 에 의존하지만 런타임에서는 AmontDiscountPolicy, PercentDiscountPolicy에 의존한다.

    다른 새로운 구현DiscountPolicy 를 추가해도 기존 PercentDiscountPolicy,AmontDiscountPolicy,DiscountPolicy 는 수정하지 않았다. 이는 즉 개방폐쇄원칙을 지켰다 할수 있다.

    이렇게 컴파일 타임 의존성은 유지 하면서

    런타임 의존성을 확장, 수정하는것이 개방폐쇄 원칙을 따르게 해준다.

    이를 가능하게 하기 위해서는 추상화에 의존해야한다.

    공통적인 부분은 문맥이 바뀌어도 바뀌지 않고 추상화에 생략된 부분에 확장의 여지를 남겨야한다.

    Swift 언어에서는 추상클래스가 없다. protocol을 사용하여 추상화가 가능하지만 protocol에서 함수를 구현 하거나 initial value 를 넣을수 없다.

    생성 사용 분리

    Movie 클래스에서 DiscountPolicy 구현클래스 생성과 사용을 분리 함으로써 클래스 내부의 결합도가 낮아질수있었다. 이를 분리해야 추상화에만 의존할수가 있었다.

    하지만 Movie클래스를 사용하는 Client 에서는

    Movie클래스를 생성한다. 이렇게 Client 에서는 Movie 에대한 결합도가 높아졌다.

    이를 피하려면 Movie 를 사용하는 Client 가 아닌

    Client 의 Clinet 가 Movie를 생성하는것이다.

    class Factory {
    	func createAvatarMovie() {
    		return Movie("avatar",Duration(120),Money.wons(10000),AmountDiscountPolicy(Money.wons(800), SequenceCondition(1),PeriodCondition(DayOfWeek.Monday),PeriodCondition(DayOfWeek.Thursday))
    	}
    }
    
    class Client {
    	private let factory: Factory
    	init(factory:Factory){
    		self.factory = factory
    	}
    	func getFee() -> Int{
    		let avatar:Movie = factory.createAvatarMovie()
    		return avatar.getFee()
    	}
    }
    	
    

    이렇게 Movie와 DiscountPolicy를 생성하는 책임을 Factory로 보냈다.

    이렇게 Client도 Movie도 필요한 객체들을 사용만하게 되었다.

    순수한 가공물

    애플리케이션 설계에서 책임 할당을 할때 도메인모델 을 참조해 도메인에 존재하는 개념을 표현하는 객체를 만들어 책임을 주었다. 이를 표면적분해라 한다.

    예로 Movie와 DiscountPolicy같은 객체들은 도메인에 존재하는 개념들이다. 근데 Factory는? 도메인에 존재하지 않는 인공적인 개념이다.

    이를 행위적 분해라고도 하고 이런 인공적인 객체를 순수가공물이라고도 부른다.

    객체지향 애플리케이션은 사실 인공적인 객체들이 도메인 객체보다 더 많은 비중을 차지하고있다. 도메인설계는 시작일뿐이다.

    현대 도시가 공원,호수,건물 도로 등 인공물로 가득차 있듯이

    똑같다. 주저 하지말고 인공객체를 창조하라.

    Service Locator

    service locator 는 의존성 객체들을 보관하는 저장소이다. 객체가 의존할 객체가 필요할때 serviceLocator 가 전달해준다

    class ServiceLocator {
    	private static instance: ServiceLocator = ServiceLocator()
    	private discountPolicy: DiscountPolicy
    	func discountPolcy() -> DiscountPolocy {
    		return instance.discountPolocy
    	}
    	func setDiscountPolicy(discountPolicy:DiscountPolicy) {
    		instance.discountPolicy = discountPolicy
    	}
    }
    
    

    이제 Movie 객체가 필요한 discountPolicy 객체들을 주입받아 사용할수 있다.

    그러나 이거의 단점은 의존성을 감춘다는것이다.

    Movie 인터페이스에서 discountPolicy가 필요하다는것을 알수없고 잊어버리고 주입하지 않으면 null 에러가 발생하게된다.

    이렇게되면 문제를 컴파일 시점이 아닌 런타임시점에 알게된다.

    이런경우 단위 테스트도 어려워진다. 모든 단위테스트에 serviceLocator 상태를 공유해야하는데 이는 단위 테스트가 각각 고립돼야하는것을 위반한다.

    이거의 문제점은 의존성이 캡슐화를 위반했다는것이다.

    퍼블릭 인터페이스 만으로 이해할수 있는 코드가 캡슐화의 훌륭한 코드이다. 구현 내부를 샅샅이 뒤져야 이해할수 있다면 좋은게 아니다.

    의존성 역전

    Movie 객체가 만약 가격 계산을위해 AmountDiscountPolicy에 의존한다면 문제가 된다. 하위 수준의 AmountDiscountPolicy를 PercentDiscountPolicy로 변경한다고 Movie가 영향을 받으면 안된다.

    (Movie 의 역할인 가격계산은 할인조건정보에 비해 상위 기능이다)

    상위 수준 모듈은 하위수준 모듈에 의존하면 안된다.

    이를 해결하기 위해 DiscountPolicy 추상화를 만들어서

    Movie가 DiscountPolicy를 의존하게 했다.

    이러면 AmountDiscountPolicy 또한 DiscountPolicy를 의존하게 된다.

    이를 의존성 역전이라 한다.

    댓글

Designed by Tistory.