소프트웨어공학

[SOLID] OCP (Open-Closed Principle)

타우루스 2025. 10. 3. 21:08

SOLID 원칙 중에서 OCP, 즉 개방-폐쇄 원칙(Open-Closed Principle) 에 대해 함께 알아볼까요? 🚀

객체 지향 설계의 핵심 원칙 중 하나인 OCP를 제대로 이해하면, 더 유연하고 유지보수하기 좋은 코드를 작성할 수 있게 될 거예요.

 

✨ OCP 학습 계획 ✨

  1. OCP 기본 개념 이해하기: OCP가 무엇인지 쉽고 재미있는 비유로 알아봐요.
  2. OCP의 중요성 파악하기: 왜 개발자들이 OCP를 중요하게 생각하는지 알아보고, OCP를 지키면 어떤 장점이 있는지 살펴봐요.
  3. OCP 실제 코드 예제로 적용하기: 간단한 코드를 통해 OCP를 위반한 경우와 잘 지킨 경우를 비교하며 확실하게 개념을 익혀봐요.

1. OCP 기본 개념 이해하기

개방-폐쇄 원칙(Open-Closed Principle, OCP)을 한마디로 정의하면 다음과 같아요.

"소프트웨어 개체(클래스, 모듈, 함수 등)는 확장에는 열려 있어야 하고(Open for Extension), 수정에는 닫혀 있어야 한다(Closed for Modification)."

 

이게 무슨 말일까요? 쉽게 비유를 들어 설명해 드릴게요.

 

혹시 스마트폰을 사용해 보셨죠? 📱 우리가 스마트폰에 새로운 기능을 추가하고 싶을 때, 예를 들어 은행 앱이나 게임을 설치하고 싶을 때, 스마트폰 본체를 열어서 내부 회로를 바꾸거나 부품을 고치지 않잖아요. 그냥 앱스토어에서 새로운 앱을 '설치'하기만 하면 되죠.

 

 

여기서 스마트폰 본체는 '수정에는 닫혀 있는(Closed for Modification)' 부분이에요. 이미 완성되어 안정적으로 동작하고 있기 때문에 함부로 건드리지 않죠. 반면에 앱스토어를 통해 새로운 앱(기능)을 얼마든지 추가할 수 있는 것은 '확장에는 열려 있는(Open for Extension)' 부분이에요.

 

OCP는 바로 이런 개념을 코드에 적용하는 거예요. 기존에 잘 작동하던 코드는 그대로 두고, 새로운 기능을 추가하고 싶을 땐 새로운 코드를 작성해서 '추가'하는 방식으로 시스템을 설계하는 원칙이죠.

 

2. OCP의 중요성 파악하기

OCP를 잘 지키면 우리 코드가 훨씬 건강해지고 관리하기 편해져요. 크게 세 가지 장점이 있습니다.

1. 코드의 안정성 증가 🛡️

가장 큰 장점이에요. 이미 테스트까지 마치고 잘 동작하는 기존 코드를 수정할 필요가 없으니, 새로운 기능을 추가하다가 기존 기능에 버그를 만들 위험이 크게 줄어들어요. "긁어 부스럼 만들지 않는다"는 속담처럼, 잘 돌아가는 코드는 그대로 두는 것이 가장 안전하겠죠?

2. 유지보수 효율성 향상 🛠️

새로운 요구사항이 생길 때마다 거대한 if-else 문이나 switch 문을 수정하는 건 정말 골치 아픈 일이에요. OCP를 따르면, 우리는 새로운 기능을 위한 클래스만 하나 추가하면 끝나요. 어디를 고쳐야 할지 고민할 필요 없이, 그냥 새 코드를 추가하면 되니 유지보수가 훨씬 간편해집니다.

3. 높은 확장성과 유연성 🤸

시스템이 레고 블록처럼 유연해져요. 새로운 결제 수단, 새로운 알림 방식, 새로운 파일 포맷 등 어떤 기능이 필요하든, 관련 부품(클래스)을 만들어서 그냥 '장착'만 하면 돼요. 덕분에 변화하는 요구사항에 빠르고 유연하게 대처할 수 있는 시스템을 만들 수 있습니다.

특히 IT 프로젝트를 관리할 때, 시스템이 얼마나 유연하게 변경사항을 수용할 수 있는지는 시스템의 품질을 평가하는 아주 중요한 척도예요. OCP는 바로 그런 고품질 시스템을 만드는 핵심 비결 중 하나랍니다!


3. OCP 실제 코드 예제로 적용하기

상황: 온라인 쇼핑몰의 결제 시스템을 만든다고 가정해 봅시다. 처음에는 신용카드 결제 기능만 필요했지만, 나중에 사업이 확장되면서 카카오페이네이버페이 기능도 추가해야 하는 상황입니다.

 


OCP를 위반한 예시 (나쁜 설계 👎)

먼저 OCP를 생각하지 않고 코드를 작성해 볼게요.

# 나쁜 예시
class PaymentProcessor:
    def process(self, amount, payment_type):
        if payment_type == "credit_card":
            print(f"신용카드로 {amount}원 결제를 처리합니다.")
        elif payment_type == "kakao_pay":
            print(f"카카오페이로 {amount}원 결제를 처리합니다.")
        elif payment_type == "naver_pay":
            print(f"네이버페이로 {amount}원 결제를 처리합니다.")

# 사용 예시
payment_processor = PaymentProcessor()
payment_processor.process(10000, "credit_card")
payment_processor.process(5000, "kakao_pay")

 

문제점: 이 코드는 당장은 잘 작동하는 것처럼 보입니다. 하지만 여기에 '토스페이'를 추가하려면 어떻게 해야 할까요?

 

PaymentProcessor 클래스의 process 메서드에 elif payment_type == "toss_pay": 라는 코드를 수정해야만 합니다.

 

기존 코드를 직접 수정했기 때문에 이 설계는 "수정에 닫혀 있어야 한다" 는 OCP 원칙을 위반한 것입니다. 결제 수단이 100개가 되면 if-elif 문이 100개가 달린 끔찍한 코드가 되겠죠? 😱


OCP를 적용한 예시 (좋은 설계 👍)

이제 OCP를 적용해서 코드를 개선해 보겠습니다. 먼저 UML 다이어그램으로 전체 구조를 살펴볼까요?

 

UML 다이어그램

 

  • PaymentMethod (결제 방식 인터페이스): 모든 결제 방식들이 따라야 할 규칙(pay 메서드)을 정의한 '설계도'입니다.
  • CreditCardPayment, KakaoPayPayment 등: PaymentMethod라는 설계도를 따라서 실제 기능을 구현한 '구현체' 클래스들입니다.
  • PaymentProcessor (결제 처리기): 어떤 결제 방식이 오든, 그저 설계도(PaymentMethod)에 정의된 pay 메서드를 호출하기만 합니다. 실제 어떤 종류의 결제 방식인지는 신경 쓰지 않아요.

코드 구현

 

이제 위 UML 구조를 코드로 옮겨보겠습니다.

from abc import ABC, abstractmethod

# 1. 결제 방식에 대한 '설계도(인터페이스)'를 만든다.
class PaymentMethod(ABC):
    @abstractmethod
    def pay(self, amount):
        pass

# 2. 각 결제 방식을 '설계도'에 맞춰 독립된 클래스로 구현한다.
class CreditCardPayment(PaymentMethod):
    def pay(self, amount):
        print(f"신용카드로 {amount}원 결제를 처리합니다.")

class KakaoPayPayment(PaymentMethod):
    def pay(self, amount):
        print(f"카카오페이로 {amount}원 결제를 처리합니다.")

class NaverPayPayment(PaymentMethod):
    def pay(self, amount):
        print(f"네이버페이로 {amount}원 결제를 처리합니다.")

# 3. 결제 처리기는 '설계도'에만 의존한다. (수정할 필요가 없음!)
class PaymentProcessor:
    def process(self, amount, payment_method: PaymentMethod):
        # 어떤 결제 방식이든 pay 메서드를 호출
        payment_method.pay(amount)

# --- 사용 예시 ---
processor = PaymentProcessor()

# 신용카드 결제
credit_card = CreditCardPayment()
processor.process(10000, credit_card)

# 카카오페이 결제
kakao_pay = KakaoPayPayment()
processor.process(5000, kakao_pay)

# --- 새로운 기능 추가 ---
# 만약 '토스페이'를 추가하고 싶다면?
class TossPayPayment(PaymentMethod):
    def pay(self, amount):
        print(f"토스페이로 {amount}원 결제를 처리합니다.")

# PaymentProcessor 클래스는 전혀 수정하지 않고 새로운 클래스만 추가하면 끝!
toss_pay = TossPayPayment()
processor.process(3000, toss_pay)

 

개선된 점: 보이시나요? 새로운 결제 수단인 TossPayPayment를 추가할 때, 기존의 PaymentProcessor 클래스는 단 한 줄도 건드리지 않았습니다. 그냥 새로운 클래스를 확장했을 뿐이죠.

 

이것이 바로 수정에는 닫혀 있고(PaymentProcessor), 확장에는 열려 있는(새로운 PaymentMethod 구현체 추가) 완벽한 OCP 구조입니다!

 

✨ OCP(Open-Closed Principle) 핵심 요약 ✨

1. OCP 정의:

  • "소프트웨어 개체는 확장에는 열려 있어야 하고, 수정에는 닫혀 있어야 한다."
  • 기존의 안정적인 코드는 건드리지 않고, 새로운 기능은 기존 코드를 '확장'하는 방식으로 추가해야 한다는 원칙입니다.

2. OCP 비유:

  • 스마트폰 앱 설치: 스마트폰 본체를 수정하지 않고 앱(기능)을 추가하는 것처럼, 기존 시스템을 수정 없이 확장할 수 있어야 합니다.
  • 레고 블록 쌓기: 기존의 성을 부수지 않고 새로운 블록을 쌓아 올리듯이, 새로운 기능을 추가할 때 기존 코드를 수정하지 않고 새 코드를 추가해야 합니다.

3. OCP의 중요성 (왜 필요한가?):

  • 코드의 안정성 증가: 기존 코드를 수정할 일이 없으므로, 버그 발생 위험이 줄어듭니다.
  • 유지보수 효율성 향상: 새로운 요구사항에 대한 변경이 기존 코드 수정 대신 새로운 코드 추가로 이루어져 유지보수가 용이합니다.
  • 높은 확장성과 유연성: 변화하는 요구사항에 빠르게 대처할 수 있는 유연한 시스템을 구축할 수 있습니다.

4. OCP 적용 방법 (코드로 어떻게 구현하나?):

  • 추상화 활용: 인터페이스(Interface)나 추상 클래스(Abstract Class)를 활용하여 변경될 수 있는 부분을 추상화합니다.
  • 다형성 활용: PaymentProcessor가 PaymentMethod 인터페이스에만 의존하도록 하고, 실제 결제 방식(신용카드, 카카오페이 등)은 이 인터페이스를 구현하는 별도의 클래스로 분리합니다.
  • 의존성 역전 원칙(DIP)과도 밀접: 구체적인 구현체보다는 추상화된 것에 의존하게 함으로써, 상위 모듈이 하위 모듈에 영향을 받지 않도록 합니다.

5. 결론: OCP는 변화에 유연하게 대응하고, 안정적이며, 유지보수하기 쉬운 고품질 소프트웨어를 만드는 데 필수적인 설계 원칙입니다.