SOLID 원칙, 개방 폐쇄의 원칙(Open/Closed Principle)
이번 solid 원칙은 'O'에 해당하는 개방폐쇄의 원칙입니다. 확장에는 열려있고 수정에는 닫혀있는 코드 디자인에 대하여 탐구해보도록 하겠습니다.
이전 글에서 solid 원칙의 'S' 인 단일 책임의 원칙을 다뤄보았습니다. 개방 폐쇄의 원칙은 solid 원칙의 두번째 원칙입니다.
소프트웨어의 엔티티(클래스, 모듈, 함수 기타 등등)는 확장을 위해서는 열려있어야하지만, 수정을 위해서는 닫혀있어야 합니다.
해당 원리를 이용함으로써 목표는 해당 모듈의 소스코드를 수정하지 않고도 모듈의 행위를 확장하는 것입니다.
상품에 할인을 적용하는 시나리오를 생각해보겠습니다. 할인 서비스는 지정된 금액을 할인하고 할인된 금액을 되돌려주게 됩니다.
아래의 예제에서는 모든 성인에 대해서 적용하는 한가지 할인에 대한 값만 있다고 가정하겠습니다.
package com.blackdog.solid.ocp;
import java.math.BigDecimal;
import java.math.RoundingMode;
public class Discount {
public BigDecimal apply(BigDecimal price) {
BigDecimal percent = new BigDecimal("0.10");
BigDecimal discount = price.multiply(percent);
return price.subtract(discount.setScale(2, RoundingMode.HALF_UP));
}
}
그리고 할인 서비스(DiscountService)는 이 할인을 제공된 가격에 적용합니다.
package com.blackdog.solid.ocp;
import java.math.BigDecimal;
public class DiscountService {
public BigDecimal applyDiscount(BigDecimal price,Discount discount) {
return discount.apply(discountPrice);
}
}
그런데 노인 할인이 필요하여 노인 할인에 대한 시나리오도 필요하게 되었습니다.
package com.blackdog.solid.ocp;
import java.math.BigDecimal;
import java.math.RoundingMode;
public class SeniorDiscount {
public BigDecimal apply(BigDecimal price) {
BigDecimal percent = new BigDecimal("0.20");
BigDecimal discount = price.multiply(percent);
return price.subtract(discount.setScale(2, RoundingMode.HALF_UP));
}
}
이는 할인 서비스가 성인 할인과 노인 할인에 대해서 모두 고려해야하는 상황이기 때문에 할인 서비스를 매우 복잡하게 만듭니다.(불필요한 두개의 할인 서비스 기능이 필요하게 됩니다.)
package com.blackdog.solid.ocp;
import java.math.BigDecimal;
public class DiscountService {
// 성인할인
public BigDecimal applyDiscount(BigDecimal price,Discount discount) {
return discount.apply(price);
}
// 노인할인
public BigDecimal applySeniorDiscount(BigDecimal price,SeniorDiscount discount) {
return discount.apply(price);
}
}
이렇게 개발함으로써 우리는 할인 서비스의 행동을 확장하기 위해(성격이 다른 할인의 도입 등) 소스코드를 수정해야만 합니다. 또한 추가적으로 다른 할인에 대해서는 반드시 다른 메소드가 추가되어야만 하는 상황입니다. 이를 개방 폐쇄의 원칙을 도입하여 수정해보도록 하겠습니다.
개방 폐쇄의 원칙을 도입하기 위해 우선 할인이라는 Discount 인터페이스를 설계하겠습니다.
package com.blackdog.solid.ocp;
import java.math.BigDecimal;
public interface Discount {
BigDecimal apply(BigDecimal price);
}
기본 할인인 성인 할인은 AdultDiscount 클래스로 이름을 변경하고 Discount 인터페이스를 구현하도록 합니다.
package com.blackdog.solid.ocp;
import java.math.BigDecimal;
import java.math.RoundingMode;
public class AdultDiscount implements Discount {
@Override
public BigDecimal apply(BigDecimal price) {
BigDecimal percent = new BigDecimal("0.10");
BigDecimal discount = price.multiply(percent);
return price.subtract(discount.setScale(2, RoundingMode.HALF_UP));
}
}
노인할인의 기능을 할 SeniorDiscount 클래스도 Discount 인터페이스를 구현하여 생성해줍니다.
ackage com.blackdog.solid.ocp;
import java.math.BigDecimal;
import java.math.RoundingMode;
public class SeniorDiscount implements Discount {
@Override
public BigDecimal apply(BigDecimal price) {
BigDecimal percent = new BigDecimal("0.20");
BigDecimal discount = price.multiply(percent);
return price.subtract(discount.setScale(2, RoundingMode.HALF_UP));
}
}
마지막으로, DiscountService 클래스가 Discount 인터페이스를 기반으로 동작하도록 하기 위해서 리팩토링됩니다.
package com.blackdog.solid.ocp;
import java.math.BigDecimal;
public class DiscountService {
public BigDecimal applyDiscounts(BigDecimal price, Discount[] discounts) {
BigDecimal discountPrice = price.add(BigDecimal.ZERO);
for(Discount discount : discounts) {
discountPrice = discount.apply(discountPrice);
}
return discountPrice;
}
}
이러한 방법으로 DiscountService는 Discount의 구현체에 의존하지 않고 Discount 인터페이스에 의해 오버라이딩된 apply 메소드에 의해 할인이 적용되어 소스코드를 수정하지 않고 다양한 할인 시나리오를 도입할 수 있습니다.(Extension Open)
이는 할인 인터페이스에도 적용해볼 수 있습니다. 할인이 적용될 때 기본 할인을 추가적으로 적용해야 한다고 했을 때를 가정해보겠습니다.
package com.blackdog.solid.ocp;
import java.math.BigDecimal;
import java.math.RoundingMode;
public class BasicDiscount implements Discount {
@Override
public BigDecimal apply(BigDecimal price) {
BigDecimal percent = new BigDecimal("0.01");
BigDecimal discount = price.multiply(percent);
return price.subtract(discount.setScale(2, RoundingMode.HALF_UP));
}
}