앞서 SRP에 대해 알아보았다. 하나의 클래스는 하나의 책임만 담당해야 한다. 여기서 책임이란, 단순하게 하나의 메소드를 뜻하는게 아니라 하나의 특정 액터를 위한 기능 집합이다. SRP를 지킴으로써 결합도가 낮은 코드를 작성 할 수 있게 된다.

이번에는 OCP, 개방 폐쇄. 원칙에 대해 알아보기로 한다.

OCP - Open/CLosed Principle

Software entitieds (class, modules, functions, etc.) should be open for extension, but closed for modification. 소프트웨어 엔티티(클래스, 모듈, 함수 등)는 확장에 대해서는 열려 있어야 하지만 변경에 대해서는 닫혀 있어야 한다. 이 말은 즉슨 아래와 같은 문장과 같은 의미다.
(사용되는 기능)의 확장에는 열려있고, (기능을 사용하는 코드의) 변화에 대해서는 닫혀 있어야 한다. 이러한 확장과 변화를 위해 자바에서는 주로 아래 예시를 보며 자신의 확장과, 주변의 변화가 어떤것인지 알아볼 것이다.

예시

OCP는 실생활에도 너무나도 많이 쓰이는 원칙이다. 아래 가장 흔한 예를 들어볼 것이다.

실무에서나 공부할때도 가장 많이 쓰이는 JDBC이다. JDBC를 사용하는 클라이언트는 데이터베이스가 오라클에서 Mysql로 변경되어도 Connection을 설정하는 부분 외에는 따로 수정할 필요가 없다. 심지어 Connection 설정 부분을 별도의 설정 파일로 분리해두면, 클라이언트 코드는 단 한줄도 변경할 필요가 없다. 여기서 각각의 입장에서 바라봐보자.
JDBC 입장에서는, 새로운 DBMS가 등장하여도 해당 JDBC 인터페이스만 구현하면 되기 때문에 확장에는 열려있다. Application의 입장에서는 JDBC 인터페이스를 완충 장치로 받아 변화에 영향을 받지 않는다. 곧 Application은 주변의 변화에 닫혀 있다고 할 수 있는 것이다.

추가 실사례

  • JVM : 운영체제에 독립되어 Java 프로그램을 실행할 수 있도록 한다.
  • 스프링 프레임워크

잘못된 예시와 개선 사례 (1)

다운 캐스팅을 하여 개방 폐쇄 원칙이 깨지는 현상이 있다. 아래와 같은 인터페이스와 클래스가 있다.

public interface Character{
	public void draw();
}

public class Player implements Character{
	public void draw();
}

public class Enemy implements Character{
	public void draw();
}

public class Missile implements Character{
	public void draw();
}

여기서 아래와 같은 메소드를 적용하여 다운캐스팅이 발생하는 케이스가 있다.

public void drawCharacter(Character character) {
  if(character instanceof Missile) {  // 타입 확인
    Missile missile = (Missile) character; // 타입 다운 캐스팅
    missile.drawSpecific();

  } else {
    character.draw();

  }

}

여기서 characterMissile타입일 경우 별도로 처리를 하고 있다. 만일, Character를 구현하는 클래스가 많아져 또다른 별도 처리를 진행해야 한다면, 해당 메소드에서 추가로 별도 처리를 진행해주어야 한다. 즉, 변경에는 닫혀 있지 않은 것이다.
위와 같이 보통 instanceof와 같은 타입 확인 연산자를 사용할 경우, 개방 폐쇄 원칙을 지키지 않았을 가능성이 높다. 위와 같은 현상을 개선하기 위해 아래와 같이 수정해보았다.

public interface Character{
	public void draw();
	public void drawSpecific();
}

public class Player implements Character{
	public void draw();
	public void drawSpecific();
}

public class Enemy implements Character{
	public void draw();
	public void drawSpecific();
}

public class Missile implements Character{
	public void draw();
	public void drawSpecific();
}


public void drawCharacter(Character character) {
    character.draw();
    character.drawspecific(); // 추가로 그려야 할 부분을 그리기
}

잘못된 예시와 개선 사례 (2)

비슷한 if-else 블록이 존재한다.

앞서 작성한 Character 클래스를 상속받은 Enemy 클래스가 있다고 가정한다. 정해진 패턴에 따라 경로를 이동하는 코드가 필요할 경우, 아래와 같이 작성할 수 있다.

public class Enemy extends Character {
  private int pathPattern;
  public Enemy(int pathPattern) {
    this.pathPattern = pathPattern;
  }

  public void draw() {
    if(pathPattern == 1) {
      x += 4;

    } else if(pathPattern == 2) {
      y += 10;

    } else if(pathPattern == 4) {
      x += 4;
      y += 10;

    }

    ...;  // 그려 주는 코드

  }

}

위 코드에서는 pathPattern이 새로 추가될 때마다 draw()메소드에 if문 분기처리를 추가해주어야 한다. 이 말은 즉슨 Enemy클래스의 변경에 닫혀있지 않다는 것이다. 따라서, (x,y)와 같은 경로를 추상화 하여 아래와 같이 작성한다.

public class Enemy extends Character {
  private PathPattern pathPattern;
  public Enemy(PathPattern pathPattern) {
    this.pathPattern = pathPattern;

  }

  public void draw() {
    int x = pathPattern.nextX();
    int y = pathPattern.nextY();

    ...; // 그려 주는 코드

  }

}

이렇게 작성하면, pathPattern이 어떻게 변하더라도, 새로운 것이 생기더라도 draw()메소드 내부 코드는 변동이 없는 것을 확인할 수 있다.

결론

개방 폐쇄 원칙은 유연함과 관련된 원칙이다. 변화하는 부분을 추상화하여 기존 코드를 수정하지 않고도 확장을 할 수 있게 한다. 이를 통해 객체 지향 프로그래밍의 가장 큰 장점인 유연성, 재사용성, 유지보수성을 얻을 수 있다.


참고 자료

  1. 스프링 입문을 위한 객체 지향의 원리와 이해
  2. [객체지향 SW 설계의 원칙] ① 개방-폐쇄 원칙
  3. 개방 폐쇄 원칙(Open - Closed Principle)
  4. SOLID: Part 2 - The Open/Closed Principle

oksusutea's blog

꾸준히 기록하려고 만든 블로그