SOLID 두 번째 단어인 개방 폐쇄 원칙Open-Closed Principle에 대해 알아보도록 하자.

개방 폐쇄 원칙이란?

SOFTWARE ENTITIES (CLASSES, MODULES, FUNCTIONS, ETC.) SHOULD BE OPEN FOR EXTENSION, BUT CLOSED FOR MODIFICATION.

Robert Martin. OCP: The Open-Closed Principle.

어떤 변경 사항이 의존하는 모듈 들에 연속적으로 영향을 미친다면, 이는 깨지기 쉽고fragile, 경직되고rigid, 예측할 수unpredictable 없으며, 재사용 불가능한unreusable 디자인이다. OCP는 이러한 문제를 해결하기 위한 방법 중 하나이며, OCP를 따르는 모듈은 다음의 두 가지를 만족시킨다.

  • 확장에 열려 있음Open For Extension
  • 수정에 닫혀 있음Closed for Modification

요구 사항이 변경될 때 새로운 코드를 추가함으로써 행위를 확장해야지, 이미 동작하고 있는 기존 코드를 변경해서는 안된다는 이야기이다. 서로 모순되는 것 처럼 보이는 이 두 가지 속성은 추상화를 통해 만족시킬 수 있다. 특정 객체의 행위가 필요할 때는 인터페이스로 소통하고, 행위 추가시에는 구현체를 추가하는 것이다. 인터페이스는 상대적으로 잘 변하지 않으므로 수정에는 닫혀 있고, 구현체는 얼마든지 추가할 수 있으므로 확장에는 열려 있을 수 있다.

간단한 예제를 통해 좀 더 생각해 보도록 하자.

그림 1. 확장에 닫힌 구조

위 그림에서 Drawer는 도형을 화면에 그리는 역할을 담당하며, 대상이 되는 도형은 Rectangle과 Circle 클래스이다. 이를 코드로 표현하면 다음과 같다.

public class Drawer {
   public void draw(List<Shape> shapes) {
       shapes.forEach(shape -> {
           switch (shape.getShapeType()) {
               case CIRCLE:
                   Circle circle = (Circle) shape;
                   circle.draw();
                   break;
               case RECTANGLE:
                   Rectangle rectangle = (Rectangle) shape;
                   rectangle.draw();
                   break;
           }
       });
   }
}

public enum ShapeType {
   CIRCLE, SQUARE;
}

public interface Shape {
   ShapeType getShapeType();
}

public class Circle implements Shape {
   public ShapeType getShapeType() {
       return ShapeType.CIRCLE;
   }

   public void draw() {
       System.out.print("circle-draw");
   }
}

public class Rectangle implements Shape {
   public ShapeType getShapeType() {
       return ShapeType.RECTANGLE;
   }

   public void draw() {
       System.out.print("rectangle-draw");
   }
}

만약 새로운 종류의 Shape이 추가되면 어디를 수정해야 할까? 우선 ShapeType의 값을 추가해야 하고, 새로운 Shape 구현체를 추가해야 하며, Drawer의 draw 메서드에 새로운 case를 선언해야 한다. Shape이 추가될 때 마다 이 작업은 반복된다. 이 코드는 그나마 단순하지만, 실제 세상에서는 switch나 if/else를 찾느라 고생 좀 해야 할지도 모른다.

이런 문제를 해결하기 위한 구조의 그림은 다음과 같다.

그림 2. 확장에 열린 구조

코드는 다음과 같다.

public class Drawer {
   public void draw(List<Shape> shapes) {
       shapes.forEach(shape -> {
           shape.draw();
       });
   }
}

public interface Shape {
   void draw();
}

public class Circle implements Shape {
   public void draw() {
       System.out.print("circle-draw");
   }
}

public class Rectangle implements Shape {
   public void draw() {
       System.out.print("rectangle-draw");
   }
}

행위 확장시에는 Shape 구현체를 추가하면 되므로 열린 상태가 되고, Drawer 입장에서는 행위 추가로 인한 수정이 필요 없으므로 닫힌 상태가 된다.

얼마나 닫혀 있을 수 있을까?

만약 Shape 인터페이스에 메서드가 추가되면 어떠한가? 혹은 Circle과 Rectangle을 동시에 그릴 수 없다는 제약이 생긴다면? 기존 Shape 구현체들을 수정하거나 Drawer의 메서드를 수정해야 할지도 모른다. 닫혀 있던 부분들이 더 이상 닫혀 있지 않게 되는 것이다.

In general, no matter how “closed” a module is, there will always be some kind of change against which it is not closed.

Since closure cannot be complete, it must be strategic. That is, the designer must choose the kinds of changes against which to close his design. This takes a certain amount of prescience derived from experience.

– Robert Martin. OCP: The Open-Closed Principle.

모듈의 닫힘 정도와 관계 없이, 닫혀 있을 수 없는 변경은 항상 존재한다고 한다. 이에 따라 전략적 닫힘strategic closure이 필요하게 되는데, 이는 결국 어떤 부분을 변경에 대해 닫혀 있도록 할지 결정해야 한다는 이야기이다. 이 결정은 직관에 따를 수도 있고 경험 또는 데이터 등에 기반할 수도 있겠다.

로버트 마틴의 글에서는 이를 설명하기 위해 Circle 보다 Rectangle을 먼저 그려야 한다는 제약을 Drawer에 추가한다. 이를 만족시키기 위한 방법으로는 2가지가 언급되는데, 각각은 서로 다른 곳에서 열린 변경이 발생한다. 첫 번째는 Drawer를 수정에 닫혀 있도록 하는 방법이다. 이는 Shape 구현체들이 스스로 우선순위를 결정하게 함으로써 가능하다. Java로 구현하고자 한다면 Shape 구현체들이 Comparable을 구현하게 하고, Drawer에서는 Collections.sort를 이용할 수 있겠다.

하지만 Shape이 추가되거나 Shape에 대한 정렬 정책이 바뀔 때 마다, 각 Shape 구현체들에 대한 수정이 불가피하게 된다. Shape 구현체들이 수정에 대해 닫혀 있고자 한다면, Shape 구현체들의 우선순위를 담은 자료 구조형 클래스를 생각해 볼 수 있다. 이 우선순위를 기반으로 Shape 구현체들을 정렬하는 메서드만 만들어 두면, Shape이 추가되거나 정렬 정책 변경 시에 이 자료 구조 클래스만 수정해 주면 된다.

좀 더 확장 해 보기

만약 Shape의 타입과 상관 없는 정렬을 지원해야 한다면 어떨까? 닫힘에 대해 좀 더 생각해 볼 이슈라면서 로버트 마틴이 던지는 질문이다. 다만, OrderedObject라는 추상화(OrderedShape이라는 클래스가 Shape과 OrderedShape로부터 파생)를 이용하라고 한다. 코드나 다이어그램 등 더 이상의 설명은 없어 정확히 예측은 어렵지만, 개인적으로 이런 코드를 이야기하고 있는 게 아닌가 싶다. 작성된 코드는 아래 링크에서 확인할 수 있다.

생각해 볼 거리

  • 전략적 닫힘을 고려할 때, Factory Pattern은 어떠한가?

참고 문헌

[1] Robert Martin. OCP: The Open-Closed Principle.

관련 문서

[1] SOLID 개요
[2] 단일 책임 원칙 SRP
[3] 개방 폐쇄 법칙 OCP
[4] 리스코프 치환 법칙 LSP