지금까지 작성한 코드는 결합도가 높아 요구 사항이 변경되었을 때 수정해주어야 할 코드가 상대적으로 많다.
CozDiscountCondition에서 FixedRateDiscountPolicy를 직접 생성해서 사용한다. 따라서, CozDiscountCondition은 FixedRateDiscountPolicy에 직접적으로 의존.
KidDiscountCondition에서 FixedAmountDiscountPolicy를 직접 생성해서 사용한다. 따라서 KidDiscountCondition은 FixedAmountDiscountPolicy에 직접적으로 의존.
Order에서 CozDiscountCondition을 직접 생성해서 사용한다. 따라서 Order는 CozDiscountCondition에 직접적으로 의존.
Order에서 KidDiscountCondition을 직접 생성해서 사용한다. 따라서 Order는 KidDiscountCondition에 직접적으로 의존.
즉, 특정 객체가 어떤 객체를 사용할지 직접 결정한다는 것입니다.
문제의 원인 : 특정 객체가 어떤 객체를 사용할지 직접 결정한다.
→ 역할이 아니라 구현에 의존하고 있다.
각 클래스들이 구현에 의존하는 것이 아니라 역할에 의존하게 하면 된다.
할인 정책, 할인 조건에 해당하는 역할을 만들고, 해당 역할에 의존하도록 하면 된다.
역할을 정의할 때 사용할 수 있는 것이 바로 인터페이스이다.
위의 의존 관계를 아래와 같이 수정. 즉, 구현에 의존하도록 하는 것이 아니라, 역할에 의존하도록 만듬. 특정 역할을 인터페이스로 정의해 두고, 인터페이스를 implements하는 객체라면 어떤 객체이든 화살표 우항의 역할을 수행할 수 있게 하는 것이 핵심이다.
CozDiscountCondition → DiscountPolicy 인터페이스
KidDiscountCondition → DiscountPolicy 인터페이스
Order → DiscountCondition 인터페이스
그다음, FixedRateDiscountPolicy와 FixedAmountDiscountPolicy에서 제공하는 메서드인 calculateDiscountedPrice()를 인터페이스의 추상 메서드로 등록
FixedRateDiscountPolicy와 FixedAmountDiscountPolicy가 DiscountPolicy를 구현
여기까지 DiscountPolicy 인터페이스를 생성하였고, 할인 정책 구현 클래스들이 인터페이스를 구현하도록 하였다. 여기에서 인터페이스는 역할에 해당하며, FixedRateDiscountPolicy와 FixedAmountDiscountPolicy는 구현에 해당
앞서 정의한 인터페이스를 활용해서 CozDiscountCondition이 역할에 해당하는 인터페이스에 의존하게끔 해주어야 한다.
먼저, 아래와 같이 FixedRateDiscountPolicy의 인스턴스를 저장하고 있는 필드를 삭제하고, 인터페이스 타입의 필드를 정의해 준 다음, 생성자를 만들어준다.
인터페이스는 타입으로 사용될 수 있다. 즉, DiscountPolicy 타입의 필드 discountPolicy에는 DiscountPolicy 인터페이스를 구현한 FixedRateDiscountPolicy와 FixedAmountDiscountPolicy가 할당될 수 있다.
그다음, applyDiscount()에서 fixedRateDiscountPolicy가 아니라, 방금 정의한 discountPolicy에서 메서드를 참조하게끔 수정해 준다.
CozDiscountCondition의 생성자에 매개변수가 추가되었으니, Order의 makeOrder()에서 CozDiscountCondition을 인스턴스화하는 코드에 인자를 추가해주어야 한다.
의존성 주입이란, 객체가 자신이 의존할 객체를 외부에서 주입해 주는 것을 의미한다.
방금 코드를 수정함으로써 CozDiscountCondition이 스스로 DiscountPolicy 객체를 생성해서 사용하지 않게 되었으며, CozDiscountCondition 객체가 생성되는 시점에 DiscountPolicy역할을 수행할 객체를 외부로부터 생성자를 통해 주입받게 되었다.
참고로, 여기에서 의존성 주입을 위해 사용한 방법은 생성자 주입이다. 생성자의 인자로 객체를 전달했고 그 객체가 DiscountCondition의 구현 클래스에 전달되어 객체 내부의 discountPolicy 필드에 할당되었다. 이때, DiscountPolicy 타입의 discountPolicy에 구현 클래스의 객체가 할당될 수 있는 것은 다형성 덕분이라는 사실에 주목하자!
즉, 의존성 주입의 기반 원리는 추상화와 다형성이다. 인터페이스를 통해 공통된 메서드들을 추상화하여 추상메서드로 정의하였고, 인터페이스를 타입으로 사용한 필드를 정의함으로써 다형성을 통해 구현 클래스의 객체를 할당받을 수 있기 때문이다.
결과적으로,
CozDiscountCondition은 DiscountPolicy의 구현 클래스가 무엇이더라도, 인터페이스에 정의된 메서드를 통해 calculateDiscountedPrice()를 사용할 수 있게 되었다.
또한, 할인 정책을 바꿀 때도 CozDiscountCondition의 코드를 전혀 수정하지 않아도 된다.
코드스테이츠 수강생에게 고정 금액을 할인하도록 이벤트가 변경된다면 makeOrder()에서 그저 CozDiscountCondition을 인스턴스화할 때 생성자의 인자만 아래와 같이 바꿔주면 된다.
CozDiscountCondition cozDiscountCondition = new CozDiscountCondition(new FixedAmountDiscountPolicy(500));
CozDiscountCondition과 FixedRateDiscountPolicy가 직접적으로 강하게 결합하는 것이 아니라, CozDiscountCondition은 DiscountPolicy라는 인터페이스와 느슨하게 결합되어 있다.
CozDiscountCondition은 DiscountPolicy라는 인터페이스 덕분에, DiscountPolicy를 구현한 인스턴스만 discountPolicy에 할당받을 수 있으며, 어떤 인스턴스가 discountPolicy에 할당되더라도 DiscountPolicy를 구현한 객체라면 믿고 discountPolicy에 할당된 인스턴스의 메서드를 호출할 수 있다.
앞서 CozDiscountCondition을 수정한 것과 같은 맥락으로 수정해 주면 된다.
1.구현 클래스를 인스턴스화하여 필드에 정의하는 코드를 삭제하고,
2.인터페이스 타입의 필드를 선언한 다음,
3.생성자를 만들어주고,
4.applyDiscount()에서 fixedAmountDiscountPolicy가 아니라 discountPolicy를 통해 calculateDiscountedPrice()를 호출하도록 코드를 수정.
그다음, makeOrder()에서 KidDiscountCondition 생성자에 인자를 추가해 준다. 청소년 할인은 고정 금액 500원 할인이니 아래와 같이 추가해 주면 된다.
이제 KidDiscountCondition도 의존성 주입을 통해 스스로 구현 클래스를 결정하지 않게 되었으며, 구현 클래스가 아니라 인터페이스에 의존하게 되어 구현 클래스를 변경하여도 영향을 받지 않게 되었다.
지금까지 할인 조건과 할인 정책이 서로 의존하여 발생하는 문제는 해결하였으나, 아직 Order는 할인 조건과 관련된 구현 클래스에 의존하고 있다.
즉, 할인 조건이 변경되면 필연적으로 Order의 코드를 수정해주어야만 한다.
discountCondition 패키지에 DiscountCondition 인터페이스를 생성하고, 외부에서 사용하는 메서드들을 추상 메서드로 정의
그다음, CozDiscountCondition과 KidDiscountCondition이 DiscountCondition 인터페이스를 구현하도록 해준다.
인터페이스를 만들고 클래스들이 인터페이스를 구현하도록 하였으니, 이제 문제가 존재했던 Order로 가서 의존성 주입을 활용하여 문제를 해결해 보자.
먼저, Order에 필드로 discountConditions를 정의해 준다. 할인 조건은 여러 개이므로 아래와 같이 배열로 필드를 정의해야 한다. 이후에 Order를 인스턴스화할 때, 할인 조건을 담은 배열을 외부로부터 생성자를 통해 주입받도록 할 것이다.
discountConditions를 인자로 받도록 생성자를 수정해 줍시다. 생성자를 통해 객체 배열을 주입받게 된다.
그다음, makeOrder()에서 이제 직접 객체를 생성해서 사용하는 것이 아니라, 주입된 객체 배열인 discountConditions를 사용하도록 해야 합니다. 그를 위해서는 아래와 같은 동작이 필요하다.
기존에는 CozDiscountCondition과 KidDiscounCondition 인스턴스를 직접 생성해서 따로따로 checkDiscountCondition()을 호출해 주었지만
이제부터는 DiscountCondition[] 타입의 객체 배열인 discountConditions를 순회하면서, 각 요소를 통해 checkDiscountCondition()를 호출하도록 해줄 것이다.
기존에는 CozDiscountCondition과 KidDiscounCondition 인스턴스를 통해 직접 applyDiscount()를 호출해 주었다.
이제부터는 반복문을 순회하면서 각 요소를 통해 applyDiscount()를 호출하도록 코드를 수정해 줄 것이다.
지금까지 Order에서 외부로부터 객체를 주입받도록 했으니, 이제 외부에서 객체를 주입해 주는 코드를 작성해주어야 한다. Order의 생성자가 호출되는 OrderApp으로 가서, Order의 인자로 아래와 같이 객체들을 추가해 보자.
이제 Order 또한 직접 CozDiscountCondition과 KidDiscountCondition 인스턴스를 직접 생성하지 않게 되었으며, 구현 클래스에 의존하지 않게 되었습니다. 이제 할인 조건이 변경되어도 Order 클래스의 코드는 바꿀 필요가 없다.
만약, 새로운 할인 조건을 적용해야 한다면 해당 할인 조건과 관련된 클래스를 새로 만들어서 정의하고, Order 생성자의 인자로 새롭게 정의한 클래스의 인스턴스를 전달하면 되겠지요. 이것이 바로 변화와 확장에 유연한 객체지향 프로그래밍의 핵심 장점이다.