개방 폐쇄 원칙 - OCP (Open Closed Principle)
개방 폐쇄의 원칙(OCP)이란 기존의 코드를 변경하지 않으면서, 기능을 추가할 수 있도록 설계가 되어야 한다는 원칙을 말한다.
보통 OCP를 확장에 대해서는 개방적(open)이고, 수정에 대해서는 폐쇄적(closed)이어야 한다는 의미로 정의한다.
여기서 확장이란 새로운 기능이 추가됨을 의미한다.
따라서 해석하자면, 기능 추가 요청이 오면 클래스를 확장을 통해 손쉽게 구현하면서, 확장에 따른 클래스 수정은 최소화 하도록 프로그램을 작성해야 하는 설계 기법을 말한다고 보면 된다.
※ 참고
[ 확장에 열려있다 ]
- 모듈의 확장성을 보장하는 것을 의미한다.
- 새로운 변경 사항이 발생했을 때 유연하게 코드를 추가함으로써 애플리케이션의 기능을 큰 힘을 들이지 않고 확장할 수 있다.
[ 변경에 닫혀있다 ]
- 객체를 직접적으로 수정하는건 제한해야 한다는 것을 의미한다.
- 새로운 변경 사항이 발생했을 때 객체를 직접적으로 수정해야 한다면 새로운 변경사항에 대해 유연하게 대응할 수 없는 애플리케이션이라고 말한다.
- 이는 유지보수의 비용 증가로 이어지는 매우 안좋은 예시이다.
- 따라서 객체를 직접 수정하지 않고도 변경사항을 적용할 수 있도록 설계해야 한다. 그래서 변경에 닫혀있다고 표현한 것이다.
어렵게 생각할 필요없이, OCP 원칙은 우리가 객체 지향 프로그래밍을 하면서 질리도록 배웠던 추상화를 의미하는 것으로 보면 된다.
즉, OCP는 다형성과 확장을 가능케 하는 객체지향의 장점을 극대화하는 설계 원칙으로써, 우리는 코딩할때 강의에서 배운대로 객체를 추상화함으로써, 확장엔 열려있고 변경엔 닫혀있는 유연한 구조를 만들어 사용해오며 객체 지향 프로그래밍의 OCP 원칙의 효과를 이용해왔던 것이다.
그래서 클래스를 추가해야한다면 기존 코드를 크게 수정할 필요없이, 적절하게 상속 관계에 맞춰 추가만 한다면 유연하게 확장을 할 수 있었던 것이다.
OCP 원칙 위반 예제와 수정하기
다음 Animal 클래스가 있고, Animal 타입을 받아 고양이 혹은 개면 각각 동물의 소리에 맞춰 출력하는 HelloAnimal 클래스가 있다.
메인 메소드에서 cat과 dog 동물 객체를 만들고 HelloAnimal 클래스의 hello() 메소드를 통해 실행해보면 오류없이 잘 동작됨을 확인 할 수 있다.
class Animal {
String type;
Animal(String type) {
this.type = type;
}
}
// 동물 타입을 받아 각 동물에 맞춰 울음소리를 내게 하는 클래스 모듈
class HelloAnimal {
void hello(Animal animal) {
if(animal.type.equals("Cat")) {
System.out.println("냐옹");
} else if(animal.type.equals("Dog")) {
System.out.println("멍멍");
}
}
}
public class Main {
public static void main(String[] args) {
HelloAnimal hello = new HelloAnimal();
Animal cat = new Animal("Cat");
Animal dog = new Animal("Dog");
hello.hello(cat); // 냐옹
hello.hello(dog); // 멍멍
}
}
동작 자체는 문제가 없지만 문제는 기능 추가 이다.
만일 '고양이'와 '개' 외에 '양'이나 '사자'를 추가하게 된다면 어떻게 될까?
당연히 HelloAnimal 클래스를 수정해주어야 한다. 각 객체의 필드 변수에 맞게 if문을 분기하여 구성해줘야 한다.
public class Main {
public static void main(String[] args) {
HelloAnimal hello = new HelloAnimal();
Animal cat = new Animal("Cat");
Animal dog = new Animal("Dog");
Animal sheep = new Animal("Sheep");
Animal lion = new Animal("Lion");
hello.hello(cat); // 냐옹
hello.hello(dog); // 멍멍
hello.hello(sheep);
hello.hello(lion);
}
}
class HelloAnimal {
// 기능을 확장하기 위해서는 클래스 내부 구성을 일일히 수정해야 하는 번거로움이 생긴다.
void hello(Animal animal) {
if (animal.type.equals("Cat")) {
System.out.println("냐옹");
} else if (animal.type.equals("Dog")) {
System.out.println("멍멍");
} else if (animal.type.equals("Sheep")) {
System.out.println("메에에");
} else if (animal.type.equals("Lion")) {
System.out.println("어흥");
}
// ...
}
}
이런식으로 코드를 구성한다면, 동물이 추가될때마다 계속 코드를 일일히 변경해줘야 하는 번거로운 작업이 생기게 된다.
이는 처음부터 설계서부터 잘못되었기 때문에 일어나는 현상이다.
따라서 처음에 OCP 설계 원칙에 따라 적절한 추상화 클래스를 구성하고 이를 상속하여 확장시키는 관계로 구성하면 변경에는 닫히고(closed) 추가에는 열려있는(opened) 프로그램을 만들수 있다.
이때 어떤식으로 OCP 대로 추상화 설계를 할 것인가에 대해서는 다음 규칙대로 이행하면 된다.
- 먼저 변경(확장)될 것과 변경되지 않을 것을 엄격히 구분한다.
- 이 두 모듈이 만나는 지점에 추상화(추상클래스 or 인터페이스)를 정의한다.
- 구현체에 의존하기보다 정의한 추상화에 의존하도록 코드를 작성 한다.
// 추상화
abstract class Animal {
abstract void speak();
}
class Cat extends Animal { // 상속
void speak() {
System.out.println("냐옹");
}
}
class Dog extends Animal { // 상속
void speak() {
System.out.println("멍멍");
}
}
class HelloAnimal {
void hello(Animal animal) {
animal.speak();
}
}
public class Main {
public static void main(String[] args) {
HelloAnimal hello = new HelloAnimal();
Animal cat = new Cat();
Animal dog = new Dog();
hello.hello(cat); // 냐옹
hello.hello(dog); // 멍멍
}
}
위와 같이 구성하게 되면 기능 추가가 됬을때에도 코드 수정 없이 확장이 가능하게 된다.
따라서 다음과 같이 양 클래스와 사자 클래스를 추가할때 HelloAnimal 클래스의 코드 수정 없이 정상적으로 기능 확장이 되는 것을 보여주게 된다.
// 추상클래스를 상속만 하면 메소드 강제 구현 규칙으로 규격화만 하면 확장에 제한 없다 (opened)
class Sheep extends Animal {
void speak() {
System.out.println("매에에");
}
}
class Lion extends Animal {
void speak() {
System.out.println("어흥");
}
}
// 기능 확장으로 인한 클래스가 추가되어도, 더이상 수정할 필요가 없어진다 (closed)
class HelloAnimal {
void hello(Animal animal) {
animal.speak();
}
}
public class Main {
public static void main(String[] args) {
HelloAnimal hello = new HelloAnimal();
Animal cat = new Cat();
Animal dog = new Dog();
Animal sheep = new Sheep();
Animal lion = new Lion();
hello.hello(cat); // 냐옹
hello.hello(dog); // 멍멍
hello.hello(sheep); // 매에에
hello.hello(lion); // 어흥
}
}
OCP 원칙을 따른 JDBC
OCP 원칙의 가장 잘 따르는 예시가 바로 자바의 데이터베이스 인터페이스인 JDBC이다.
만일 자바 애플리케이션에서 사용하고 있는 데이터베이스를 MySQL에서 Oracle로 바꾸고 싶다면, 복잡한 하드 코딩 없이 그냥 connection 객체 부분만 교체해주면 된다.
즉, 자바 애플리케이션은 데이터베이스라고 하는 주변의 변화에 닫혀(closed) 되어 있는 것이다. 반대로 데이터베이스를 손쉽게 교체한다는 것은 데이터베이스가 자신의 확장에는 열려 있다는 말이 된다.
OCP 원칙 적용 주의점
확장에는 열려있고 변경에는 닫히게 하기 위해서는 추상화를 잘 설계할 필요성이 있는데, 추상화(추상 클래스 or 인터페이스)를 정의할 때 여러 경우의 수에 대한 고려와 예측이 필요하다.
보통 우리는 추상화라는 개념에 대해 '구체적이지 않은' 정도의 의미로 느슨하게 알고만 있다. 하지만 '그래디 부치(Grady Booch)'에 의하면 '추상화란 다른 모든 종류의 객체로부터 식별될 수 있는 객체의 본질적인 특징'이라고 정의한다.
즉, 추상 메서드 설계에서 적당한 추상화 레벨을 선택함으로써, 어떠한 행위에 대한 본질적인 정의를 서브 클래스에 전파함으로써 관계를 성립되게 하는 것이다.
만일 이러한 추상화에 따른 상속 구조를 처음부터 이상하게 구성하게 되면, 다음에 배울 LSP(리스코프 치환 원칙) 과 ISP(인터페이스 분리 원칙) 위반으로 이어지게 된다.
또한 OCP는 DIP(의존 역전 원칙)의 설계 기반이 되기도 한다.
따라서 이부분은 오로지 개발자의 역량에 달려 있다고 해도 과언이 아니다. 많은 경험과 경력만이 역량을 키울 수 있다.
참고