단일 책임 원칙이 변경에 관해 다뤘다면 개방 폐쇄 원칙은 확장에 대해 다룬다. 이 원칙은 “확장에는 열려있고 변경에는 닫혀 있어야 한다”라는 말로 표현되기도 한다.
이 원칙의 주 목적은 기존 코드를 수정하지 않으며 확장 가능한 시스템을 만드는 것이다. 시스템 운영에서 코드를 변경하는 것은 아주 위험하다. 큰 프로젝트에서는 변경하려는 코드를 사용하는 액터가 여럿인 경우 코드 변경이 아주 어렵다. 따라서 코드를 확장하고자 할 때 기존 코드를 건드리지 않는 것이 최고의 전략이다.
위반 예시와 잘 지켜진 예시 코드를 보며 이해해보자
// ----------------- Shape interface -----------------
public interface Shape { }
// ----------------- Rectangle class -----------------
public class Rectangle implements Shape {
private double width;
private double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
public double getWidth() {
return width;
}
public double getHeight() {
return height;
}
}
// ----------------- Circle class -----------------
public class Circle implements Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
public double getRadius() {
return radius;
}
}
// ----------------- AreaCalculator (위반 예시) -----------------
public class AreaCalculator {
public double calculateTotalArea(List<Shape> shapes) {
double totalArea = 0;
for (Shape shape : shapes) {
if (shape instanceof Rectangle) {
Rectangle rectangle = (Rectangle) shape;
totalArea += rectangle.getWidth() * rectangle.getHeight();
} else if (shape instanceof Circle) {
Circle circle = (Circle) shape;
totalArea += Math.PI * circle.getRadius() * circle.getRadius();
}
// 새로운 Shape 타입이 생길 때마다 코드를 수정해야 함
// else if (shape instanceof Triangle) { ... }
}
return totalArea;
}
}
개방 폐쇠 원칙을 위반한 예시이다. AreaCalculator 라는 넓이를 계산하는 클래스가 따로 존재하면 도형의 종류가 추가 될 때마다 AreaCalculator의 calculateTotalArea 메서드의 내부 코드가 수정되어야 한다. 이 경우에는 수정에 닫혀 있는 개방 폐쇠 원칙을 지키지 못했다고 볼 수 있다.
// ----------------- Shape interface -----------------
public interface Shape {
double getArea(); // 각 도형 스스로 넓이를 계산하도록
}
// ----------------- Rectangle class -----------------
public class Rectangle implements Shape {
private double width;
private double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double getArea() {
return width * height;
}
}
// ----------------- Circle class -----------------
public class Circle implements Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double getArea() {
return Math.PI * radius * radius;
}
}
// ----------------- Triangle class (새로운 도형) -----------------
public class Triangle implements Shape {
private double base;
private double height;
public Triangle(double base, double height) {
this.base = base;
this.height = height;
}
@Override
public double getArea() {
return (base * height) / 2;
}
}
// ----------------- AreaCalculator (OCP 준수 예시) -----------------
public class AreaCalculator {
public double calculateTotalArea(List<Shape> shapes) {
double totalArea = 0;
for (Shape shape : shapes) {
totalArea += shape.getArea();
}
return totalArea;
}
}
이 코드에서는 인터페이스에서 역할을 정의해 각 도형이 넓이를 구하는 책임을 가지도록 만들었다. 따라서 이전 코드와 같이 도형의 종류가 추가될 때마다 if-else문을 이용한 직접적인 수정이 일어나지 않게되었다.