객체지향

[객체지향] SOLID(2) - OCP(개방-폐쇄 원칙)

재담 2022. 3. 1. 21:46

OCP(Open-Closed Principle)

개방-폐쇄 원칙이란 소프트웨어 개체(클래스, 모듈, 함수 등)는 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다는 것을 말한다. 소프트웨어 개발 작업에 이용된 많은 모듈 중, 하나에 수정을 할 때 그 모듈을 이용하는 다른 모듈을 전부 고쳐야 한다면, 이와 같은 프로그램은 수정하기가 어렵다. 개방-폐쇄 원칙은 시스템의 구조를 올바르게 리팩토링 하여 나중에 이와 같은 유형의 변경이 더 이상의 수정을 유발하지 않도록 하는 것이다. 개방-폐쇄 원칙이 잘 적용되면 기능을 추가하거나 변경해야 할 때 이미 제대로 동작하고 있던 원래 코드를 변경하지 않아도, 기존의 코드에 새로운 코드를 추가함으로써 기능의 추가나 변경이 가능하다.

 

개방-폐쇄 원칙의 두 가지 속성은 다음과 같은 의미를 가지고 있다.

  • 확장에 열려 있다. : 모듈의 동작을 확장할 수 있다는 것을 의미한다. 어플리케이션의 요구 사항이 변경될 때, 이 변경에 맞게 새로운 동작을 추가해 모듈을 확장할 수 있다. 즉, 모듈이 하는 일을 변경할 수 있다.
  • 변경에 닫혀 있다. : 모듈의 소스 코드나 바이너리 코드를 수정하지 않아도 모듈의 기능을 확장하거나 변경할 수 있다. 모듈의 실행 가능한 바이너리 형태나 링크 가능한 라이브러리를 건드릴 필요가 없다.

 

개방-폐쇄 원칙은 객체 지향 프로그래밍의 핵심 원칙이라고 할 수 있다. 이를 무시하고 프로그래밍을 한다면, 객체 지향 프로그래밍의 가장 큰 장점인 유연성, 재사용성, 유지보수성 등을 결코 얻을 수 없다. 따라서 객체 지향 프로그래밍 언어에서 개방-폐쇄 원칙은 반드시 지켜야 할 기본적인 원칙이다.

 

OCP의 예시를 살펴보자.

public class Product {
    private String name;
    private int price;
    private String option; // 새상품, 중고...

    public Product(String name, int price, String option) {
        this.name = name;
        this.price = price;
        this.option = option;
    }

    public int getPrice() {
        return price;
    }

    public String getOption() {
        return option;
    }
}
public class ProductValidator {
    public void validatePrice(Product product) throws IllegalArgumentException {
        if (product.getOption().equals("New")) {
            if (product.getPrice() < 1000) {
                throw new IllegalArgumentException("New product needs more than 1000");
            }
        } else if (product.getOption().equals("Old")) {
            if (product.getPrice() < 500) {
                throw new IllegalArgumentException("Old product needs more than 500");
            }
        }
    }
}

위와 같은 상황에서 상품의 종류가 추가된다면, ProductValidator 클래스에서 옵션을 추가하는 것이 좋을까? 옵션이 추가됐다고 해서 ProductValidator 클래스를 변경하는 것은 OCP에 어긋난다. 다음과 같이 Validator를 추상화하여 옵션이 추가될 때 확장해주는 것이 바람직해 보인다.

public interface Validator {
    boolean support(Product product);

    void validate(Product product) throws IllegalArgumentException;
}
public class DefaultValidator implements Validator {
    @Override
    public boolean support(Product product) {
        return product.getOption().equals("New");
    }

    @Override
    public void validate(Product product) throws IllegalArgumentException {
        if (product.getPrice() < 1000) {
            throw new IllegalArgumentException("New product needs more than 1000");
        }
    }
}
public class OldProductValidator implements Validator {

    @Override
    public boolean support(Product product) {
        return product.getOption().equals("Old");
    }

    @Override
    public void validate(Product product) throws IllegalArgumentException {
        if (product.getPrice() < 500) {
            throw new IllegalArgumentException("New product needs more than 500");
        }
    }
}
public class ProductValidator {
    private final List<Validator> validators = Arrays.asList(
            new DefaultValidator(),
            new OldProductValidator()
    );

    public void validate(Product product) {
        Validator validator = new DefaultValidator();

        for (Validator localValidator : validators) {
            if (localValidator.support(product)) {
                validator = localValidator;
                break;
            }
        }

        validator.validate(product);
    }
}

Reference