2025-08-08 23:57

Tags:

개방-폐쇄 원칙 (Open-Closed Principle) 핸드북

1. 만들어진 이유: 소프트웨어의 ‘유연성’과 ‘안정성’

소프트웨어를 개발하고 운영하다 보면 필연적으로 변경이라는 요구사항에 직면합니다. 새로운 기능을 추가하거나 기존 로직을 수정해야 하는 상황은 언제나 발생합니다.

초기 개발 단계에서는 이런 변경이 간단할 수 있습니다. 하지만 시스템이 복잡해지고 코드가 서로 얽히기 시작하면, 작은 수정 하나가 예상치 못한 곳에서 큰 문제를 일으키는 **‘나비 효과’**를 경험하게 됩니다. 예를 들어, 할인 정책을 하나 추가하기 위해 결제 시스템의 핵심 코드를 수정했더니, 기존의 모든 결제 로직이 영향을 받아 장애가 발생하는 식입니다.

**개방-폐쇄 원칙(OCP)**은 바로 이러한 문제를 해결하기 위해 탄생했습니다. 1988년 버트런드 마이어(Bertrand Meyer)가 그의 저서에서 처음 제시한 이 원칙의 핵심 목표는 다음과 같습니다.

“소프트웨어 개체(클래스, 모듈, 함수 등)는 확장에 대해서는 열려 있어야 하고, 수정에 대해서는 닫혀 있어야 한다.”

  • 확장에 대해 열려 있다 (Open for Extension): 새로운 기능이나 요구사항이 생겼을 때, 기존 코드를 변경하지 않고도 시스템의 동작을 추가하거나 변경할 수 있어야 합니다.

  • 수정에 대해 닫혀 있다 (Closed for Modification): 한번 만들어지고 테스트까지 완료된 기존의 코드는 특별한 이유(버그 수정 등)가 없는 한 수정되어서는 안 됩니다. 이는 이미 검증된 코드의 안정성을 보장하기 위함입니다.

쉽게 비유하자면, 잘 만들어진 스마트폰과 같습니다. 우리는 새로운 기능을 사용하고 싶을 때 스마트폰 본체를 뜯어서 내부 회로를 바꾸지 않습니다. 대신 ‘앱’이라는 형태로 기능을 **‘확장’**하여 설치합니다. 스마트폰 본체(기존 코드)는 수정에 닫혀 있고, 앱 스토어(확장 포인트)를 통해 기능 확장에 열려 있는 것입니다.

OCP를 지키면 다음과 같은 장점을 얻을 수 있습니다.

  • 유지보수성 향상: 새로운 기능을 추가할 때 기존 코드를 건드리지 않으므로, 사이드 이펙트의 위험이 줄어듭니다.

  • 유연성 및 확장성 확보: 시스템이 변화에 쉽게 적응할 수 있는 구조가 됩니다.

  • 재사용성 증가: 기존의 안정적인 코드를 그대로 유지하며 새로운 곳에 재사용하기 용이합니다.

2. 구조: ‘추상화’를 통한 분리

OCP를 구현하는 핵심적인 방법은 **추상화(Abstraction)**에 의존하는 것입니다. 즉, 구체적인 구현 클래스가 아닌 **인터페이스(Interface)나 추상 클래스(Abstract Class)**에 의존하도록 코드를 설계하는 것입니다.

변하기 쉬운 것과 변하지 않는 것을 분리하고, 이 둘 사이에 추상화라는 다리를 놓아 서로에게 직접적인 영향을 주지 않도록 만드는 것이 핵심 구조입니다.

OCP를 위반하는 구조

// 할인 종류에 따라 분기문이 계속 추가되어야 하는 구조
class PaymentService {
    public double calculateDiscount(String discountType, double price) {
        if ("SUMMER_SALE".equals(discountType)) {
            return price * 0.8; // 여름 세일 20%
        } else if ("VIP_MEMBER".equals(discountType)) {
            return price * 0.9; // VIP 회원 10%
        }
        // 새로운 할인 정책이 추가될 때마다 이 클래스를 '수정'해야 한다.
        // 이는 OCP를 위반한다.
        return price;
    }
}

위 코드는 새로운 할인 정책(CHRISTMAS_SALE 등)이 추가될 때마다 PaymentService 클래스의 calculateDiscount 메서드를 직접 수정해야 합니다. 이는 ‘수정에 닫혀 있어야 한다’는 원칙을 명백히 위반합니다.

OCP를 준수하는 구조

OCP를 준수하기 위해 ‘할인 정책’이라는 변하기 쉬운 개념을 인터페이스로 추상화합니다.

1. 추상화 계층 (인터페이스) 정의

// 할인 정책에 대한 추상화 인터페이스
interface DiscountPolicy {
    double applyDiscount(double price);
}

2. 구체적인 구현 클래스 생성

각각의 할인 정책을 인터페이스를 구현한 별도의 클래스로 만듭니다.

// 여름 세일 정책
class SummerSalePolicy implements DiscountPolicy {
    @Override
    public double applyDiscount(double price) {
        return price * 0.8;
    }
}

// VIP 회원 정책
class VipMemberPolicy implements DiscountPolicy {
    @Override
    public double applyDiscount(double price) {
        return price * 0.9;
    }
}

// (새로운 기능) 크리스마스 세일 정책
class ChristmasSalePolicy implements DiscountPolicy {
    @Override
    public double applyDiscount(double price) {
        return price * 0.75; // 25% 할인
    }
}

3. 추상화에 의존하는 클라이언트 코드

PaymentService는 이제 구체적인 할인 정책 클래스가 아닌, DiscountPolicy 인터페이스에만 의존합니다.

class PaymentService {
    // 구체적인 구현이 아닌 추상화(인터페이스)에 의존
    public double calculateDiscount(DiscountPolicy policy, double price) {
        // 어떤 할인 정책이 오든, PaymentService 코드는 수정되지 않는다.
        return policy.applyDiscount(price);
    }
}

이제 ChristmasSalePolicy와 같은 새로운 할인 정책이 추가되어도 PaymentService 코드는 단 한 줄도 수정할 필요가 없습니다. 그저 새로운 정책 클래스를 만들어 PaymentService에 전달하기만 하면 됩니다. 이것이 바로 확장에는 열려 있고, 수정에는 닫혀 있는 구조입니다.

3. 사용법: 전략 패턴(Strategy Pattern)의 활용

위에서 살펴본 OCP 준수 구조는 디자인 패턴 중 하나인 **전략 패턴(Strategy Pattern)**의 전형적인 예시입니다. 전략 패턴은 특정 계열의 알고리즘(전략)들을 정의하고, 각 알고리즘을 캡슐화하여 상호 교체가 가능하도록 만드는 패턴입니다.

  • 컨텍스트(Context): PaymentService처럼 전략을 사용하는 주체.

  • 전략(Strategy): DiscountPolicy 인터페이스.

  • 구체적인 전략(Concrete Strategy): SummerSalePolicy, VipMemberPolicy 등 인터페이스를 구현한 클래스들.

OCP를 코드에 적용하고 싶다면, 다음과 같은 질문을 스스로에게 던져봐야 합니다.

“지금 작성하는 코드에서 앞으로 변할 가능성이 높은 부분은 무엇인가?”

그것이 바로 추상화의 대상이 되어야 할 부분입니다. 예를 들면 다음과 같은 것들이 있습니다.

  • 데이터를 저장하는 방식 (파일, MySQL, MongoDB, …)

  • 결제 수단 (신용카드, 계좌이체, 카카오페이, …)

  • 알림을 보내는 방식 (이메일, SMS, 푸시 알림, …)

  • 데이터를 출력하는 형식 (JSON, XML, CSV, …)

이처럼 변화의 방향성을 예측하고, 해당 부분을 인터페이스로 분리하여 언제든지 새로운 구현체(전략)로 갈아 끼울 수 있도록 설계하는 것이 OCP를 실천하는 핵심적인 방법입니다.

4. 심화 내용: OCP는 ‘만병통치약’이 아니다

OCP는 훌륭한 원칙이지만, 모든 코드에 맹목적으로 적용해야 하는 것은 아닙니다. 모든 것을 추상화하고 분리하려는 시도는 오히려 시스템을 지나치게 복잡하게 만드는 **과잉 설계(Over-engineering)**로 이어질 수 있습니다.

  • 변화 가능성이 거의 없는 코드: 앞으로 변경될 확률이 거의 없는 코드까지 OCP를 적용하여 분리하는 것은 불필요한 복잡성만 증가시킵니다.

  • 비용과 시간: 추상화 계층을 만들고 코드를 분리하는 데는 추가적인 시간과 노력이 필요합니다. 프로젝트의 일정과 리소스를 고려해야 합니다.

따라서 현명한 개발자는 변화가 예상되는 지점을 현명하게 예측하고, 그 부분에 OCP를 전략적으로 적용합니다. 처음부터 완벽한 OCP 구조를 만들기보다는, 변화의 필요성이 명확해졌을 때 **리팩토링(Refactoring)**을 통해 OCP를 만족하는 구조로 개선해 나가는 것이 더 현실적인 접근 방식일 수 있습니다.

결론적으로 개방-폐쇄 원칙은 코드의 수정 없이도 기능을 유연하게 확장할 수 있도록 만드는 강력한 설계 원칙입니다. 이 원칙을 잘 이해하고 적재적소에 활용한다면, 변화에 강하고 안정적인 소프트웨어를 만들 수 있을 것입니다.

이 핸드북이 개방-폐쇄 원칙을 이해하는 데 도움이 되었기를 바랍니다. 혹시 더 궁금하거나 자세히 알고 싶은 부분이 있으신가요?

References

개방 폐쇄 원칙