Goals
- OOP의 설게 원칙 ‘SOLID’중 ‘O’를 이해한다.
- OCP를 쓰는 방법을 알아본다.
- python 예제를 통해 OCP를 이해한다.
- OCP 설계에서 주의해야할 점을 알아본다.
OCP의 유래
OCP(Open-Closed Principle)은 Bertrand Meyer의 ‘Object Oriented Software Construction’(1988)이라는 책에서 유래된 단어이다. 그러다 1996년 Robert C. Martin이 “The Open-Closed Principle”에서 이 접근 방법에 관해 설명하면서 더 유명해졌다.
OCP의 의미
OCP를 한글로 해석해보면 개방-폐쇄의 원칙이다. 코드를 개방한다는 말과 폐쇄한다는 말의 의미는 무엇일까? 기존의 코드(클래스, 모듈, 함수 등등)는 변경하지 않으면서(closed) 기능을 추가할 수 있도록(open) 설계되어야 한다는 뜻이다. 개발 작업을 하는 도중 클래스에 수정할 상황이 생긴다. 그런 상황이 생겼을 때, 수정한 클래스를 이용하는 다른 클래스를 줄줄이 고쳐야 한다면 고쳐야 할 클래스가 많아진다. 이를 위해서 OCP는 코드의 구조를 올바르게 리팩토링하여 나중에 이와 같은 유형의 변경이 더 이상의 수정을 유발하지 않도록 한다.
OCP를 쓰는 방법
- 변하는 것과 변하지 않는 것을 정확하게 구분한다. 변하는 것은(open) 가능한 한 변하기 쉽게 변하지 않는 것은(closed) 변하는 것에 영향받지 않게 설계하는 것이다.
- 두 클래스가 만나는 지점에 인터페이스를 정의한다. (아래의 예제에서
@abstractmethod
가 붙은 부분이다) 이때, 인터페이스는 변하는 것과 변하지 않는 모듈의 교차점으로 서로를 보호하는 보호막 같은 역할을 한다.
OCP를 지키지 않는 예
- 가장 대표적인 예를 가지고 설명하겠다.
class Rectangle(object):
def __init__(self, width, height):
self.width = width
self.height = height
class AreaCalculator(object):
def __init__(self, shapes):
self.shpaes = shapes
def total_area(self):
total = 0
for shape in self.shapes:
total += shape.width * shape.height
return total
def main():
shapes = [Rectangle(2, 3), Rectangle(2, 4)]
calculator = AreaCalculator(shapes)
print("전체 넓이: {}".format(calculator.total_area())) # 전체 넓이: 14
if __name__ == "__main__":
main()
만약 이 코드에서 사각형이 아닌 삼각형에 대한 넓이를 계산하는 클래스를 넣는다고 하면 어떤 일을 해야 하는지 생각해보자. 우선은 Triangle
이라는 클래스를 만들어야 하며 AreaCalculator
에 있는 total_area
메소드를 사용할 수 없으므로 Triangle
에 맞는 total_area
메소드를 만들어야 한다. 닫혀 있지 않은 상태이므로 삼각형뿐만 아니라 다른 도형도 추가한다고 생각해보면 계속 클래스와 메소드를 추가해야 한다.
OCP를 잘 지킨 예
from abc import abstractmethod
class Shape(object):
@abstractmethod
def area(self):
pass
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
class AreaCalculator(object):
def __init__(self, shapes):
self.shapes = shapes
def total_area(self):
total = 0
for shape in self.shapes:
total += shape.area
return total
def main():
shapes = [Rectangle(1, 6), Rectangle(2, 3)]
calculator = AreaCalculator(shapes)
print("전체 넓이: {}"format(calculator.total_area)) # 전체 넓이: 12
if __name__ == '__main__':
main()
코드에서 다른 모양의 넓이(삼각형, 사다리꼴 등등)를 계산해주는 AreaCalculator
의 기능을 확장한다고 한다면, 우리는 Shape
의 클래스를 만들면 되고 다른 코드(AreaCalculator
의 total_area
메소드)는 수정할 필요가 없다. 즉, 기존 코드(AreaCalculator
클래스)에 대해서는 닫혀있고(closed) 새로운 Shape
클래스에 대해서는 열려있다(open).
OCP 주의해야할 점
- 확장되는 것과 변경되지 않는 모듈을 분리하는 과정에서 크기 조절을 잘해야 한다. 크기 조절에 실패하면 관계가 더 복잡해져서 설계를 망치는 경우가 있기 때문이다.
- 인터페이스는 가능하면 변경해서는 안 된다.
- 인터페이스 설계에서 적당한 추상화1 레벨을 선택하는 것이 중요하다.
참고 사이트
-
그래디 부치(Grady Booch)에 의하면 ‘추상화란 다른 모든 종류의 객체로부터 식별될 수 있는 객체의 본질적인 특징’이라고 정의하고 있다. ↩