OCP(Open-Closed Principle)는 1988년 Bertrand Meyer가 “Object-Oriented Software Construction"에서 처음 제안했다. 마틴은 이 원칙이 소프트웨어 아키텍처를 연구하는 근본적인 이유라고 말한다.
OCP의 정의
마이어는 이렇게 정의한다: 소프트웨어 개체(클래스, 모듈, 함수 등)는 확장에 대해 열려 있어야 하고, 수정에 대해 닫혀 있어야 한다(Meyer, Object-Oriented Software Construction, 1988). 다만 마이어의 1988년 원안은 구현 상속을 통한 확장을 전제로 했다 — 아래에서 다룰 인터페이스·다형성 기반의 해법은 이후 마틴이 1996년 논문 “The Open-Closed Principle"에서 재해석한 현대적 형태다.
두 가지 의미
- 확장에 열림 (Open for Extension): 새로운 기능을 추가할 수 있어야 한다
- 수정에 닫힘 (Closed for Modification): 기존 코드를 변경하지 않아야 한다
언뜻 보면 모순처럼 보인다. 새 기능을 추가하면서 기존 코드를 수정하지 않는다니?
어떻게 가능한가?
추상화의 힘
비밀은 추상화(Abstraction)에 있다. 인터페이스나 추상 클래스를 통해:
- 고정된 부분(인터페이스)은 닫혀 있음
- 변하는 부분(구현)은 열려 있음
| |
flowchart TB
subgraph Closed [닫힌 부분 - 수정 안 함]
I[ReportGenerator Interface]
end
subgraph Open [열린 부분 - 확장 가능]
PDF[PdfReportGenerator]
Excel[ExcelReportGenerator]
HTML[HtmlReportGenerator]
Future[미래의 Generator...]
end
PDF -->|구현| I
Excel -->|구현| I
HTML -->|구현| I
Future -.->|구현| I
재무 보고서 예제
마틴은 재무 보고서 시스템 예제를 사용한다.
요구사항
- 재무 데이터를 웹 페이지에 표시
- 나중에 PDF, 흑백 프린터, 컬러 프린터 출력 추가 예정
잘못된 설계
| |
OCP를 적용한 설계
flowchart TB
subgraph Core [핵심 비즈니스 로직]
FR[FinancialReporter]
FD[FinancialData]
FR --> FD
end
subgraph Boundary [경계]
RP[ReportPresenter Interface]
end
subgraph Outputs [출력 어댑터]
WP[WebPresenter]
PP[PdfPresenter]
BP[BlackWhitePrinter]
CP[ColorPrinter]
end
FR --> RP
WP -->|구현| RP
PP -->|구현| RP
BP -->|구현| RP
CP -->|구현| RP
| |
아키텍처 수준의 OCP
OCP는 클래스 수준을 넘어 아키텍처 수준에서 더 큰 의미를 가진다.
컴포넌트 계층화
flowchart TB
subgraph Level1 [가장 높은 수준 - 가장 보호됨]
BL[비즈니스 로직]
end
subgraph Level2 [중간 수준]
C[컨트롤러]
P[프레젠터]
end
subgraph Level3 [낮은 수준]
DB[(데이터베이스)]
UI[사용자 인터페이스]
end
C --> BL
P --> BL
DB --> BL
UI --> C
UI --> P
수준이 높을수록 변경으로부터 보호받는다.
방향 제어 (Directional Control)
OCP를 아키텍처 수준으로 확장하려면, 어떤 모듈이 어떤 모듈에 의존하는지를 의도적으로 통제해야 한다. 의존성의 방향을 제어하여 보호한다:
- A가 B에 의존하면, B의 변경이 A에 영향
- 따라서 중요한 것이 덜 중요한 것에 의존하면 안 됨
- 의존성 역전으로 방향을 제어
정보 은닉 (Information Hiding)
방향을 통제하는 것만으로는 부족하다. 한 모듈이 다른 모듈의 내부 구조까지 알게 되면, 그 내부가 바뀔 때 함께 깨진다. 그래서 불필요한 정보는 숨긴다:
Controller가Interactor의 내부를 알 필요 없음Presenter가Controller를 알 필요 없음- 이행적 의존성(Transitive Dependency) 제거
OCP가 아키텍처의 근본인 이유
마틴은 OCP가 시스템의 아키텍처를 떠받치는 원동력 중 하나라고 말한다(Martin, Clean Architecture, 2017).
좋은 아키텍처의 목표
- 기능 추가 시 기존 코드 변경 최소화
- 영향 범위 예측 가능
- 확장 비용 최소화
OCP를 따르면 이 목표를 달성할 수 있다.
Clean Architecture와 OCP
Clean Architecture의 동심원 구조는 OCP의 구현이다:
flowchart TB
subgraph CleanArch [Clean Architecture]
E[Entities - 가장 보호됨]
U[Use Cases]
I[Interface Adapters]
F[Frameworks - 가장 변경 가능]
end
F -->|의존| I
I -->|의존| U
U -->|의존| E
- 안쪽: 높은 수준의 정책, 변경으로부터 보호
- 바깥쪽: 세부사항, 변경 가능
- 의존성: 바깥에서 안으로만
OCP 적용 패턴
1. Strategy 패턴
런타임에 알고리즘을 교체:
| |
Strategy 패턴은 알고리즘 전체를 객체로 캡슐화해 런타임에 교체한다. Sorter는 어떤 정렬 알고리즘이 주입될지 컴파일 시점에 몰라도 되므로, 새 정렬 알고리즘을 추가해도 Sorter는 손대지 않는다. 다만 알고리즘 사이에 공유할 코드가 많다면 매 구현마다 중복이 생기기 쉽다 — 그런 경우에는 아래 Template Method가 더 적합하다.
2. Template Method 패턴
알고리즘의 골격은 고정, 세부 단계는 확장:
| |
Template Method는 Strategy와 달리 알고리즘의 골격(순서)을 상위 클래스에 고정하고, 달라지는 단계만 하위 클래스가 채워 넣는다. prepareData()처럼 모든 하위 클래스가 공유하는 로직은 상위 클래스에 한 번만 구현하면 되므로 Strategy보다 중복이 적다. 대신 상속을 쓰므로 하위 클래스가 상위 클래스의 골격에 강하게 결합되고, 런타임에 알고리즘을 바꿔 끼울 수는 없다.
3. Plugin 아키텍처
핵심에 플러그인을 꽂는 구조:
flowchart TB
Core[핵심 시스템]
P1[Plugin A]
P2[Plugin B]
P3[Plugin C]
P1 --> Core
P2 --> Core
P3 --> Core
Plugin 아키텍처는 Strategy·Template Method를 배포 단위 수준으로 확장한 것이다. 각 플러그인은 핵심 시스템이 정의한 인터페이스에만 의존하고, 핵심 시스템은 어떤 플러그인이 붙는지 전혀 모른다. 그 결과 새 플러그인을 추가·제거해도 핵심 시스템은 재컴파일·재배포할 필요가 없다 — OCP를 클래스 수준이 아니라 컴포넌트·배포 수준으로 끌어올린 형태다.
OCP의 한계
완벽한 OCP는 불가능
모든 변경에 대해 닫혀 있는 것은 불가능하다. 어떤 변경은 기존 코드 수정이 불가피하다.
전략적 폐쇄
가장 가능성 높은 변경에 대해 닫아야 한다:
- 과거에 자주 변경된 것
- 비즈니스에서 변경을 예고한 것
- 경험에 기반한 예측
첫 번째 탄환
마틴은 이를 “첫 번째 탄환을 맞는다. 그리고 그 후에 리팩토링한다"고 표현한다(Martin, Clean Architecture, 2017, 8장). 완벽한 예측은 불가능하다. 변경이 발생하면 그때 OCP를 적용하여 미래의 유사한 변경에 대비한다.
흔한 오해
“OCP는 코드를 절대 수정하지 말라는 뜻”이라는 오해가 흔하다. 실제로는 반대에 가깝다 — 처음부터 모든 변경 가능성에 대비해 인터페이스를 만드는 것은 과잉 설계다. 마틴이 “첫 번째 탄환을 맞는다"고 말했듯, 실제 변경이 한 번 일어난 뒤에야 그 지점에 추상화를 도입해 두 번째부터는 닫히게 만드는 것이 실용적이다. “Meyer의 OCP와 Martin의 OCP는 같은 것”이라는 오해도 흔한데, 앞서 보았듯 Meyer의 1988년 원안은 상속 기반이고 이 장에서 다룬 인터페이스·다형성 기반 해법은 Martin의 1996년 재해석이다.
학습 목표
이 장을 읽은 후 다음을 할 수 있어야 한다.
- OCP가 “확장에 열리고 수정에 닫힌다"는 것을 추상화·인터페이스로 어떻게 구현하는지 코드로 설명할 수 있다.
- Meyer의 1988년 원안(상속 기반)과 Martin의 1996년 재해석(인터페이스 기반)의 차이를 설명할 수 있다.
- “완벽한 OCP는 불가능하다"는 전제 아래, 어떤 변경에 우선적으로 닫아야 할지 판단할 수 있다.
참고 문헌
- Meyer, B. (1988). Object-Oriented Software Construction. Prentice Hall.
- Martin, R. C. (1996). “The Open-Closed Principle”. C++ Report.
- Martin, R. C. (2017). Clean Architecture: A Craftsman’s Guide to Software Structure and Design. Prentice Hall.
핵심 요약
| 항목 | 내용 |
|---|---|
| 정의 | 확장에 열리고, 수정에 닫힘 |
| 방법 | 추상화, 인터페이스, 다형성 |
| 아키텍처 연결 | 의존성 역전, 계층화 |
| 목표 | 변경 영향 최소화 |
| 패턴 | Strategy, Template Method, Plugin |
마틴은 이렇게 요약한다: OCP의 목표는 시스템을 확장하기 쉬운 동시에 변경으로 인해 시스템이 너무 많은 영향을 받지 않도록 하는 데 있다(Martin, Clean Architecture, 2017, 8장).
다음 장에서는
다음 장에서는 LSP: 리스코프 치환 원칙을 다룬다. 이 원칙은 Barbara Liskov가 1987년 제시하고 1994년 정식화한 것으로, 하위 타입이 상위 타입을 올바르게 대체할 수 있어야 한다는 것을 말한다.
![Featured image of post [Clean Architecture] 16. OCP: 개방-폐쇄 원칙](/post/clean-architecture/ocp-open-closed-principle/wordcloud_hu_fcf5385524b0600f.webp)
![[Clean Architecture] 14. SOLID 원칙 서론](/post/clean-architecture/solid-principles-introduction/wordcloud_hu_5dd3e02a522cd4eb.webp)
![[Clean Architecture] 15. SRP: 단일 책임 원칙](/post/clean-architecture/srp-single-responsibility-principle/wordcloud_hu_7c9c1878b0d9c6e6.webp)
![[Clean Architecture] 16. OCP: 개방-폐쇄 원칙](/post/clean-architecture/ocp-open-closed-principle/wordcloud_hu_dfede9c2fdef6f5f.webp)
![[Clean Architecture] 17. LSP: 리스코프 치환 원칙](/post/clean-architecture/lsp-liskov-substitution-principle/wordcloud_hu_f3cf91f896fa3ec8.webp)
![[Clean Architecture] 18. ISP: 인터페이스 분리 원칙](/post/clean-architecture/isp-interface-segregation-principle/wordcloud_hu_97294a781cdf31b4.webp)
![[Clean Architecture] 19. DIP: 의존성 역전 원칙](/post/clean-architecture/dip-dependency-inversion-principle/wordcloud_hu_c5b743bc5d7e577d.webp)
![[Rust] Comprehensive Rust 무료 강의 정리 및 코스 구조](/post/2022-12-30-comprehensive-rust/wordcloud_hu_d1420ff38434cdb6.webp)
![[Hardware] LattePanda Alpha에 Ubuntu 16.04 LTS 설치 가이드](/post/2018-12-06-install-ubuntu-16.04-on-lattepanda/wordcloud_hu_fc536f8de2cbd4bf.webp)