The Open Closed Principle(OCP)란?
The Open Closed Principle(OCP)는 개방-폐쇄 원칙을 말한다. 로버트 C 마틴의 글에서 소개되었다.
(출처: Design Principles and Design Patterns(https://web.archive.org/web/20150906155800/http://www.objectmentor.com/resources/articles/Principles_and_Patterns.pdf))
The Open Closed Principle (OCP)
A module should be open for extension but closed for modification.
Of all the principles of object oriented design, this is the most important. It originated from the work of Bertrand Meyer. It means simply this: We should write our modules so that they can be extended, without requiring them to be modified. In other words, we want to be able to change what the modules do, without changing the
source code of the modules.
출처: Robert C. Martin, Design Principles and Design Patterns, p.4.
(한국어 번역)
개방-폐쇄 원칙
모듈은 수정에는 닫혀있고, 확장에는 열려 있어야 한다.
객체 지향 설계의 모든 원칙 중에서 이것이 가장 중요하다. 이 원칙은 Bertrand Meyer의 작업에서 유래했다. 이 원칙의 의미는 단순하다: 우리는 모듈을 수정하지 않고도 확장할 수 있도록 작성해야 한다는 것이다. 다른 말로 말하면, 우리는 모듈의 소스 코드 변경 없이, 모듈의 동작을 변경하기를 원한다는 것이다.
객체 지향 설계 중 가장 중요한 것으로 OCP를 들고 있음을 알 수 있다.
예시 코드
그가 실제로 예로 들고 있는 코드를 살펴보자.
struct Modem {
enum Type { hayes, courrier, ernie } type;
};
struct Hayes {
Modem::Type type;
// Hayes related stuff
};
struct Courrier {
Modem::Type type;
// Courrier related stuff
};
struct Ernie {
Modem::Type type;
// Ernie related stuff
};
void DialHayes(Hayes& m, const string& pno) {
// Dial logic for Hayes
}
void DialCourrier(Courrier& m, const string& pno) {
// Dial logic for Courrier
}
void DialErnie(Ernie& m, const string& pno) {
// Dial logic for Ernie
}
void LogOn(Modem& m, string& pno, string& user, string& pw) {
if (m.type == Modem::hayes)
DialHayes((Hayes&)m, pno);
else if (m.type == Modem::courrier)
DialCourrier((Courrier&)m, pno);
else if (m.type == Modem::ernie)
DialErnie((Ernie&)m, pno);
// ...you get the idea
}
위 코드는 if else문을 통해서 특정 모뎀에 해당하는 함수를 선택하도록 하고 있다. 따라서 새로운 모뎀이 추가되거나 모뎀 정책이 변경되면 if else문을 찾아서 수정해야 한다.
로버트 마틴의 설계
동적 다형성(Dynamic Polymorphism)
위 그림은 로버트 마틴이 해결책으로 제시한 설계 방안이다. Model 인터페이스는 공통 인터페이스로 모든 모뎀이 구현해야 하는 기능을 정의한다. Hayes Modem, Courrier Modem, Ernie's Modem는 Modem 인터페이스를 구현하는 구체 클래스다. LogOn 함수는 Modem 인터페이스에만 의존하고 있다.
그가 제시한 코드 예시는 아래와 같다.
class Modem {
public:
virtual void Dial(const string &pno) = 0;
virtual void Send(char) = 0;
virtual char Recv() = 0;
virtual void Hangup() = 0;
};
void LogOn(Modem &m, string &pno, string &user, string &pw) {
m.Dial(pno);
// you get the idea.
}
추후 어떤 모뎀이 추가되더라도 LogOn 함수를 변경할 필요가 없다. 곧 LogOn 함수는 변경에 닫혀 있게 된다.
정적 다형성(Static Polymorphism)
컴파일 시 타입이 결정되는 C++의 템플릿 기능을 사용한 코드 예도 제시하고 있다.
template<typename MODEM>
void LogOn(MODEM &m, string &pno, string &user, string &pw) {
m.Dial(pno);
// you get the idea.
}
파이썬 코드 예
로버트 마틴의 코드를 파이썬 코드로 작성해보면 아래와 같다. 아래처럼 Hayes와 Courrier 두 모뎀만 있는 경우를 생각해보자.
class Hayes:
type = "hayes"
class Courrier:
type = "courrier"
def dial_hayes(modem, pno):
print(f"[Hayes] Dialing {pno}...")
def dial_courrier(modem, pno):
print(f"[Courrier] Dialing {pno}...")
def log_on(modem, pno):
if modem.type == "hayes":
dial_hayes(modem, pno)
elif modem.type == "courrier":
dial_courrier(modem, pno)
else:
raise ValueError("Unknown modem type")
if __name__ == "__main__":
log_on(Hayes(), "123-4567")
log_on(Courrier(), "234-5678")
이 상태에서 새로운 모뎀 ernie가 추가된 경우 아래처럼 log_on 코드를 변경해야 한다.
class Hayes:
type = "hayes"
class Courrier:
type = "courrier"
class Ernie:
type = "ernie"
def dial_hayes(modem, pno):
print(f"[Hayes] Dialing {pno}...")
def dial_courrier(modem, pno):
print(f"[Courrier] Dialing {pno}...")
def dial_ernie(modem, pno):
print(f"[Ernie] Dialing {pno}...")
def log_on(modem, pno):
if modem.type == "hayes":
dial_hayes(modem, pno)
elif modem.type == "courrier":
dial_courrier(modem, pno)
elif modem.type == "ernie":
dial_ernie(modem, pno)
else:
raise ValueError("Unknown modem type")
if __name__ == "__main__":
log_on(Hayes(), "123-4567")
log_on(Courrier(), "234-5678")
log_on(Ernie(), "345-6789")
OCP 원칙대로 인터페이스로 추상화해서 구현해보면 아래와 같다.
from abc import ABC, abstractmethod
class Modem(ABC):
@abstractmethod
def dial(self, pno: str):
pass
class HayesModem(Modem):
def dial(self, pno: str):
print(f"[Hayes] Dialing {pno}...")
class CourrierModem(Modem):
def dial(self, pno: str):
print(f"[Courrier] Dialing {pno}...")
def log_on(modem: Modem, pno: str):
modem.dial(pno)
if __name__ == "__main__":
log_on(HayesModem(), "123-4567")
log_on(CourrierModem(), "234-5678")
여기서 새롭게 모뎀이 추가되어도, log_on 함수는 변경할 필요가 없다.
from abc import ABC, abstractmethod
class Modem(ABC):
@abstractmethod
def dial(self, pno: str):
pass
class HayesModem(Modem):
def dial(self, pno: str):
print(f"[Hayes] Dialing {pno}...")
class CourrierModem(Modem):
def dial(self, pno: str):
print(f"[Courrier] Dialing {pno}...")
class ErnieModem(Modem):
def dial(self, pno: str):
print(f"[Ernie] Dialing {pno}...")
def log_on(modem: Modem, pno: str):
modem.dial(pno)
if __name__ == "__main__":
log_on(HayesModem(), "123-4567")
log_on(CourrierModem(), "234-5678")
log_on(ErnieModem(), "345-6789")
OCP의 아키텍처 목표
Architectural Goals of the OCP. By using these techniques to conform to the OCP, we can create modules that are extensible, without being changed. This means that, with a little forethought, we can add new features to existing code, without changing the existing code and by only adding new code. This is an ideal that can be difficult to achieve, but you will see it achieved, several times, in the case studies later on in this book.
Even if the OCP cannot be fully achieved, even partial OCP compliance can make dramatic improvements in the structure of an application. It is always better if changes do not propogate into existing code that already works. If you don’t have to change working code, you aren’t likely to break it.
출처: Robert C. Martin, Design Principles and Design Patterns, p.7.
(한국어 번역)
OCP의 아키텍처적 목표. 이러한 기법들을 활용해서 OCP를 따르다보면, 우리는 수정 없이 확장이 가능한 모듈을 만들 수 있다. 이는 곧, 약간의 사전 설계로, 기존 코드 변경 없이 새로운 코드를 추가하는 것만으로 기존 코드에 새로운 기능들을 추가할 수 있다. 이것은 실현하기 어려운 이상이지만, 당신은 달성한 몇몇 사례를 이 책에서 확인할 수 있을 것이다.
OCP를 완벽하게 지키지 못하더라도, 부분적으로 OCP를 따르는 것만으로도 애플리케이션 구조에 큰 개선 효과를 가져올 수 있다. 이미 잘 작동하는 코드에 변경이 전파되지 않는 것이 항상 더 좋다. 당신이 작동 중인 코드를 수정하지 않는다면, 그것을 망가뜨릴 가능성은 거의 없다.
마지막 단락에서 OCP의 목표를 명확히 설명해주고 있다. 작동 중인 코드를 수정하지 않으면 망가뜨릴 가능성도 적다. 수정에 닫혀 있어야 하는 이유다.