설계/설계 원칙 / / 2025. 5. 1. 14:36

SOLID의 OCP : 조건문 없이 유연하게 확장하는 개방-폐쇄 원칙

1. 기능 하나가 전체 시스템에 악영향을 주는 이유

단순한 기능 추가가 기존 시스템에 심각한 영향을 미치는 상황은 많은 개발자들이 공통적으로 겪는 현실이다.

 

클라이언트의 요청은 종종 사소해 보인다. 마치 if 문 하나만 추가하면 해결될 것처럼 느껴진다. 하지만 실제로 코드를 수정하고 나면, 예상치 못한 테스트 실패나 런타임 예외가 발생하는 일이 적지 않다. 이는 단순히 조건이 하나 늘어났기 때문이 아니라, 해당 로직이 이미 복잡하게 얽힌 의존 구조 속에 놓여 있기 때문이다. 작은 변경이 미묘한 상호작용을 유발하고, 이는 연쇄적인 문제로 확산된다.

 

이러한 상황은 일정의 압박 속에서 반복된다. 개발자는 빠르게 대응하기 위해 기존 코드 위에 또 다른 수정 코드를 덧붙이게 되고, 결과적으로 전체 구조는 누더기처럼 변해간다. 애초에는 안정적으로 운영되던 시스템이었다. 그러나 기능이 추가될 때마다 기존 로직을 직접 수정해야 하는 구조라면, 시간이 지날수록 시스템의 안정성은 점점 약화된다. 수정은 곧 위험을 수반하고, 그 위험을 회피하기 위한 방편으로 조건문과 예외 처리를 계속 추가하다 보면 코드는 복잡성과 의존성이 기하급수적으로 증가하게 된다.

 

< 기능 하나에 담긴 파장은 고요한 시스템을 절규하게 만든다. >

 

그렇다면 이러한 불안정성의 근본 원인은 무엇일까? 이는 단순히 테스트가 부족하거나 설계 문서가 미비해서 생긴 문제가 아니다. 가장 큰 문제가 외부 변화가 발생할 때마다 기존 코드를 직접 수정해야만 기능 확장이 가능한 구조 자체에 있다. 변화에 취약한 구조는 유지보수를 어렵게 만들고, 장기적으로는 기술 부채로 이어진다.

 

기능 추가는 애플리케이션이 진화하는 과정에서 필연적으로 수반되는 요구다. 하지만 매번 기존 코드를 변경해야만 확장이 가능한 시스템은 유지보수성과 안정성 측면에서 한계를 가질 수밖에 없다. 이 구조적 문제를 해결하기 위해 우리가 주목해야 할 설계 원칙이 바로 개방-폐쇄 원칙(OCP, Open-Closed Principle)이다. 이 원칙은 변화에 유연하게 대응하면서도 기존 코드를 보호할 수 있는 실질적인 해법을 제시한다.

 

2. OCP란 무엇인가? 정의 및 현실 비유

개방-폐쇄 원칙(OCP, Open-Closed Principle)은 다음과 같이 정의된다.

 

"소프트웨어의 구성 요소는 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다."

 

쉽게 말하면, 새로운 기능에 대해 얼마든지 추가할 수 있지만, 기존 코드는 수정하지 않아야 한다는 의미다. 그러나 현실에서 이러한 구조가 과연 가능할까? 개념을 보다 직관적으로 이해하기 위해 현실적인 비유를 하나 들어보자.

 

🚙 자동차를 구입한 후, 운전자는 필요에 따라 블랙박스나 내비게이션 같은 장치를 추가로 설치할 수 있다. 이때 사용자는 차량의 회로를 뜯거나 엔진 구조를 바꾸지 않는다. 대신 자동차 제조사는 미리 전원 포트나 주행 정보를 제공하는 인터페이스 등, 사전에 정의된 확장 포인트를 제공한다. 우리는 이 포인트에 장치를 연결하기만 하면 된다. 즉, 차량의 기본 구조는 손대지 않은 채, 원하는 기능을 유연하게 추가할 수 있는 것이다.

 

OCP가 지향하는 애플리케이션 구조도 이와 같다. 핵심 로직은 안정적으로 유지되며, 변화는 외부에서 유연하게 흡수될 수 있도록 설계되어야 한다. 그렇다면 OCP를 어떻게 구현할 수 있을까?

 

  • 1️⃣ : 공통된 행위는 추상화(추상 클래스 또는 인터페이스)를 통해 정의한다.
  • 2️⃣ : 서로 다른 요구사항은 해당 추상 구조를 기반으로 각각의 구현 클래스로 분리하여 추가한다.
  • 3️⃣ : 클라이언트는 구체 구현이 아닌 추상화에 의존하도록 구성한다.

 

이렇게 구성하면 새로운 기능을 추가하더라도 기존 코드를 수정할 필요가 없다. 이처럼 OCP는 변화를 구조 안에 가둘 수 있는 도구이다. 변화가 자주 발생하는 영역일수록, 이 원칙은 시스템의 안정성과 유지보수성을 보장하는 실질적인 설계 해법이 된다.

 

3. OCP 적용 전후 알림 시스템 예제

이 장에서는 가상 시나리오를 통해 OCP를 적용한 전체 코드 흐름을 예제로 다룬다. 선택한 주제는 알림 시스템이다. 알림 기능은 애플리케이션 초기에는 단순한 메시지 전송 정도로 시작하지만, 시간이 지남에 따라 다양한 채널(SMS, 이메일, 푸시 등)과 조건별 로직이 추가되면서 복잡도가 빠르게 증가하는 대표적인 영역이다. 이러한 특성 때문에 OCP의 실전 적용 예제로 적합하다고 판단하였다.

 

3-1. 초보 개발자의 OCP가 적용되지 않은 알림 시스템

처음 알림 기능이 도입된 배경은 단순했다. 주문이 완료되었을 때, 사용자에게 SMS를 통해 알림 메시지를 보내는 요구사항이 생겼고 따라서 개발자는 아래와 같은 형태로 코드를 작성했다. 해당 구조는 초기 요구사항을 충족하는 데에는 아무 문제가 없었다. 

public class NotificationService {
    public void sendNotification(String message) {
        System.out.println("Sending SMS: " + message);
    }
}

 

시간이 지남에 따라 계속적으로 새로운 요청이 들어오게 되어 이제는 이메일과 푸시 알림까지 지원을 해야 한다. 개발자는 기존 클래스에 조건문을 계속 추가하였고, sendNotification 메서드는 점점 복잡해진다.

public class NotificationService {
    public void sendNotification(String message, String type) {
        if ("sms".equals(type)) {
            System.out.println("Sending SMS: " + message);
        } else if ("email".equals(type)) {
            System.out.println("Sending Email: " + message);
        } else if ("push".equals(type)) {
            System.out.println("Sending Push Notification: " + message);
        }
    }
}

 

이와 같은 구조는 다음과 같은 문제를 안고 있다.

 

  • OCP 위반
새로운 알림 방식이 추가될 때마다 기존 메서드(sendNotification)의 코드를 직접 수정해야 한다. 이는 변경에는 닫혀 있어야 한다라는 원칙을 명확히 위반한다.

 

  •  조건문 증가로 인한 복잡도 상승
알림 종류가 늘어날수록 if-else 분기는 계속 늘어난다. 각 분기는 테스트와 디버깅 대상이 되며, 하나의 메서드가 너무 많은 책임을 지게 된다.

 

  •  유지보수의 어려움
새로운 알림 수단이 추가되거나 기존 수단에 로직 변경이 발생하면, 이 클래스의 코드에 손을 대야 한다. 이는 수정 시 부작용을 발생시킬 위험을 높인다.

 

  •  테스트 범위 확대
모든 알림 수단이 하나의 메서드에 포함되어 있기 때문에, 하나의 작은 변경에도 전체 테스트 범위를 다시 확인해야 한다. 이는 배포 주기를 늦추는 원인이 된다.

 

3-2. 능숙한 개발자의 OCP 적용 예시

이제 알림 시스템을 OCP 원칙에 따라 리팩터링 해보자. 먼저 알림 수단을 공통 인터페이스로 추상화하고, 각 구현을 별도의 클래스로 분리한다.

 

  • 1️⃣ 단계 : 알림 수단 추상화 - Notifier 인터페이스는 모든 알림 수단이 따라야 할 공통 행위를 정의한다.
public interface Notifier {
    void send(String message);
}

 

  • 2️⃣ 단계 : 각 알림 방식 구현 - 이제 각 알림 방식은 Notifier 인터페이스를 구현하는 별도의 클래스로 정의된다. 새로운 방식이 추가될 경우, 새로운 클래스를 만들기만 하면 된다.
public class SmsNotifier implements Notifier {
    public void send(String message) {
        System.out.println("Sending SMS: " + message);
    }
}

public class EmailNotifier implements Notifier {
    public void send(String message) {
        System.out.println("Sending Email: " + message);
    }
}

public class PushNotifier implements Notifier {
    public void send(String message) {
        System.out.println("Sending Push Notification: " + message);
    }
}

 

  • 3️⃣ 단계 : 사용 측 코드는 추상화에 의존 - NotificationService는 더 이상 알림 방식의 종류를 알지 못한다. 단지 Notifier 인터페이스에 정의된 계약을 따르는 객체들을 실행할 뿐이다. 새로운 알림 방식이 추가되더라도, 이 클래스의 코드는 수정될 필요가 없다.
public class NotificationService {
    private final List<Notifier> notifiers;

    public NotificationService(List<Notifier> notifiers) {
        this.notifiers = notifiers;
    }

    public void sendAll(String message) {
        for (Notifier notifier : notifiers) {
            notifier.send(message);
        }
    }
}

 

이처럼 OCP 원칙을 적용함으로써 알림 시스템은 변화에 유연하게 대응할 수 있는 구조로 리팩터링 하였다. 핵심은 알림 수단이라는 변경 가능성이 높은 요소를 추상화하고, 그 구현을 분리함으로써, 기존 코드를 수정하지 않고 기능을 확장할 수 있는 기반을 마련했다는 점이다.

 

※ 참고 : 알림 시스템을 사용하는 실행부 코드

public class Main {
    public static void main(String[] args) {
        Notifier sms = new SmsNotifier();
        Notifier email = new EmailNotifier();
        Notifier push = new PushNotifier();

        List<Notifier> notifiers = List.of(sms, email, push);

        NotificationService service = new NotificationService(notifiers);
        service.sendAll("긴급 공지: 오늘 오후 6시 시스템 점검 예정입니다.");
    }
}

 

4. OCP 적용으로 얻는 개선 효과

OCP를 적절히 적용했을 때 시스템 전반에 걸쳐 유지보수성, 확장성, 안정성을 높일 수 있다. 단순히 '코드를 덜 고친다.'라는 차원을 넘어 변화에 유연하고 충돌 없는 구조로의 전환이 일어난다.

 

실제로 OCP를 적용한 구조는 다음과 같은 이점을 제공한다.

 

  • ✅ 기존 코드의 안정성 확보
초보 개발자의 예시 코드는 새로운 알림 수단이 추가될 때마다 NotificationService 내부의 조건문을 수정해야 했다. 이는 항상 버그 발생의 위험을 동반한 것이다.

하지만 능숙한 개발자의 OCP가 적용된 구조에서는 기존 클래스를 건드릴 필요가 없다. 새로운 알림 수단은 Notifier 인터페이스를 구현한 새로운 클래스로만 정의하면 되며, 기존 서비스 로직은 그대로 유지된다. 즉, 변화가 생겨도 기존 코드는 안전하게 보호된다.

 

  • ✅ 변경 범위의 최소화
초보 개발자의 예시 코드에서는 하나의 변경이 전체 코드에 영향을 미쳤다. 알림 방식을 하나만 추가해도 조건문, 테스트, 디버깅 범위가 함께 커졌다.

하지만 능숙한 개발자의 OCP가 적용된 구조에서는 변경이 국소화된다. 예를 들어, 슬랙 알림 기능이 추가된다면 아래와 같은 클래스를 하나 추가하는 것으로 확장이 끝난다. 이처럼 변경은 하나의 클래스 내부로만 제한되며, 다른 기존 클래스는 그대로 유지된다.
public class SlackNotifier implements Notifier {
    public void send(String message) {
        System.out.println("Sending Slack Message: " + message);
    }
}

 

  • ✅ 테스트 용이성 향상
초보 개발자의 예시 코드에서는 알림 방식이 한 메서드에 몰려 있었기 때문에, 하나의 변경에도 전체 테스트를 다시 수행해야 한다.

하지만 능숙한 개발자의 OCP가 적용된 구조에서는 각 알림 구현체가 분리되어 있어 단위 테스트가 명확하게 구성 가능하다. 또한 NotificationService는 인터페이스에만 의존하므로, 테스트 시 다음과 같이 가짜 객체를 주입하여 쉽게 검증할 수 있다. 이 덕분에 기능 추가 등의 변경이 기존 기능을 깨뜨리지 않았는지를 확인하는 회귀 테스트의 부담도 줄어들고, 배포 전 검증 효율도 높아질 수 있다.

 

  • ✅ 협업과 병렬 개발이 쉬워짐
초보 개발자의 예시 코드에서는 하나의 메서드에 여러 기능이 집중되어 있어, 여러 개발자가 동시에 작업하면 충돌 가능성이 높다. 특히 Git 병합은 피하기 어려운 상황이다.

하지만 능숙한 개발자의 OCP가 적용된 구조에서는 각 기능이 클래스 단위로 독립되어 있어, 서로 다른 알림 수단을 여러 명이 동시에 개발할 수 있다. 개발 영역이 자연스럽게 분리되므로 협업의 효율성과 속도 모두 향상된다.

 

  • ✅ 미래의 변화에 대비할 수 있는 구조
OCP는 현재의 요구사항뿐 아니라 예측하지 못한 미래의 확장에도 유연하게 대응할 수 있도록 한다.

앞으로 텔레그렘, 카카오톡 등 다양한 알림 채널이 도입될 수도 있고, 각 채널의 전송 정책도 다양해질 수 있다. OCP 구조는 이러한 변화에 맞서 코드 전체를 다시 짤 필요 없이, 새로운 구현체만 추가하면 되는 확장성을 보장한다.

 

정리하면, 언급한 이점들이 추상화와 책임 분리에서 시작된다. 좋은 설계는 복잡한 기능을 단순한 구조로 수용할 수 있게 만드는 것이며, OCP가 주는 진정한 가치라고 말할 수 있다.

 

5. OCP 적용이 과도할 수 있는 경우와 판단 기준

지금까지 우리는 OCP가 코드의 확장성과 유지보수성을 어떻게 향상하는지 살펴보았다. 알림 시스템 사례를 통해 구조적 유연성이 실제로 어떤 방식으로 구현되고, 변화에 얼마나 잘 대응할 수 있는지를 확인했다. 하지만 아무리 유용한 원칙이라도, 그 적용이 과도하거나 맥락을 고려하지 않으면 오히려 설계를 해치를 요소가 될 수 있다. OCP 또한 예외는 아니다.

 

실무에서 볼 수 있는 사례 중 하나로, 변화 가능성이 적은 기능에도 무조건 인터페이스를 도입하거나, 구현체가 하나뿐인 상황에도 강제로 추상화를 적용하는 사례가 적지 않다. 예를 들어, 앞서 작성된 서비스가 내부 정책상 오직 SMS 알림만을 사용하며, 이 정책은 앞으로도 바뀌지 않을 것이라고 확정된 상황이라고 하자. 그런데도 OCP를 맹목적으로 지켜야 한다는 이유만으로 Notifier 인터페이스를 만들고, SmsNotifier 구현 클래스를 별도로 분리한다면, 그 구조는 얻는 것보다 잃는 것이 더 많아질 수 있다. 의미 없는 계층 분리는 구조를 분산시키고, 불필요한 추상화는 코드 가독성과 진입 장벽을 높이며, 시간 흐름에 따라 설계 의도가 퇴색되는 구조가 되어버릴 수 있다. 그렇기 때문에 경험 많은 설계자는 다음을 이해하고 있다.

 

  • 구조는 간결해야 유지보수가 쉽다.
  • 추상화는 필요한 경우에만 도입되어야 한다.
  • 설계는 현실적인 맥락과 변화 가능성을 기준으로 판단해야 한다.

 

< "바보는 확신에 차 있고, 현자는 의심으로 가득 차 있다." >

 

OCP는 명확한 판단 없이 무조건 적용할 수 있는 규칙이 아니다. 그 적용 범위와 시점은 설계자의 현실 인식과 판단 능력에 의해 결정된다. 진짜 숙련된 설계자는 OCP에 대해 항상 적용해야 하는 원칙으로 보지 않는다. 대신 OCP를 적용할 가치가 있는 영역에만 전략적으로 사용하는 것이 중요하다는 점을 알고 있다.

 

그렇다면 언제 OCP 적용 여부를 결정할 때 숙련된 설계자가 고려하는 대표적인 질문을 나열하면 다음과 같다.

 

  • 💡 이 기능은 자주 바뀌는가?
변화가 반복되는 영역이라면, OCP 적용이 큰 효과를 낸다. 예를 들어, 알림 시스템에서 고객 요청에 따라 알림 채널이 빈번하게 바뀌거나 추가된다면, 해당 로직은 추상화되어 있어야 한다. 이렇게 해두면 새로운 알림 수단을 도입하더라도 기존 코드를 건드릴 필요 없이, 구현 클래스를 추가하는 방식으로 확장할 수 있다.

 

  • 💡 다양한 방식으로 구현될 가능성이 있는가?
지금은 하나의 방식으로만 구현되어 있지만, 앞으로 여러 방식으로 확장될 여지가 있는 기능이라면, 추상화를 통해 구조를 유연하게 만들어 둘 필요가 있다. 예를 들어, 현재는 SMS 알림만 사용하고 있지만, 향후 이메일, 푸시, 슬랙, 텔레그램 등 다양한 채널이 추가될 가능성이 있다면, 인터페이스 기반 구조를 통해 미리 대비해 두는 것이 바람직하다. 

 

  • 💡 변경이 시스템 전체에 영향 줄 수 있는가?
특정 기능이 변경될 때 다른 모듈, 서비스, 테스트 전체에 영향을 주는 구조라면, 그 기능은 반드시 OCP 원칙에 따라 격리되어야 한다. 알림 시스템은 특히 여러 서비스에서 재사용되는 공통 컴포넌트일 가능성이 높다. 이럴 때 구조적으로 추상화되어 있지 않으면, 작은 수정이 전체 시스템에 파급 효과를 줄 수 있다.

 

  • 💡 OCP를 적용했을 때 얻는 이점이 비용보다 큰가?
OCP 적용은 언제나 개발 비용과 복잡도 증가를 수반한다. 인터페이스 분리, 구현 클래스 정의, DI 구성, 테스트 변경 등 여러 요소가 추가된다. 만약 OCP 적용으로 얻는 장점이 그 비용을 상쇄하지 못한다면, 과감히 단순한 구조를 선택하는 것도 숙련된 설계자의 판단이다.

 

6. 마무리 - 변화에 유연한 구조를 만들기 위한 설계 원칙 OCP 

지금까지의 내용을 정리하면 다음과 같다.

 

개방-폐쇄 원칙(OCP, Open-Closed Principle)은 확장에는 열려 있고, 변경에는 닫혀 있어야 한다는 설계 원칙이다. 새로운 기능을 추가할 때 기존 코드를 수정하지 않고도 시스템을 확장할 수 있게 하는 구조적 해법을 제시한다. 단순해 보이는 기능 추가도 기존 시스템에 연쇄적인 영향을 미치며, 수정이 반복될수록 코드는 조건문으로 얽힌 복잡한 구조가 된다. 매번 코드 변경을 전제로 하는 확장은 장기적으로 큰 기술 부채가 되기 때문에 OCP가 필요하다.

 

OCP는 추상화를 통해 공통 행위를 정의하고, 각기 다른 요구사항을 별도의 구현 클래스로 분리하여 구현한다. 클라이언트는 구현체가 아닌 추상화에 의존하도록 구성하여, 새로운 기능이 추가되어도 기존 코드는 그대로 유지되도록 한다. OCP를 적용하면 변화 범위가 최소화되어 시스템 안정성이 향상되고, 기능별 독립 테스트가 가능해져 품질 보증이 쉬워진다. 새로운 기능 추가가 안전해지고 배포 리스크가 줄어들며, 책임이 명확히 분리되어 협업 효율성이 높아진다.

 

다만 모든 코드에 OCP를 적용할 필요는 없다. 변화가 자주 발생하는 기능, 다양한 방식으로 구현될 기능, 변경이 시스템 전체에 영향을 줄 수 있는 기능에 우선적으로 적용한다. 무엇보다 OCP 적용으로 얻는 이점이 복잡도보다 클 때 선택해야 한다. 결국, OCP는 변화를 예측하고 그 변화의 범위를 구조 안에 가두는 설계 전략이다. 원칙의 맹목적 적용이 아닌, 변화의 특성을 파악하고 적절한 지점에 균형 있게 적용하는 설계자의 판단력이 튼튼하고 유연한 시스템을 만드는 핵심이다.

 

 

  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유