OCP(Open/Closed Principle)는 SOLID의 두 번째 원칙으로, 소프트웨어 엔티티는 확장에 대해 열려 있어야 하지만 수정에 대해서는 닫혀 있어야 한다고 규정합니다. 1988년 Bertrand Meyer가 공식화한 이 원칙은 기존 코드를 변경하지 않고 새로운 기능을 추가할 수 있게 합니다. Robert Martin의 저서 Clean Architecture (2017)에 따르면, 개방성 원칙은 추상화와 다형성을 통해 구현되며 회귀 오류의 위험을 최소화합니다.
핵심 사항
OCP(Open/Closed Principle) — 확장에 대한 개방성 및 수정에 대한 폐쇄성의 원칙입니다. 클래스, 모듈 및 함수는 소스 코드를 변경하지 않고도 새로운 동작을 추가할 수 있도록 설계되어야 합니다. 확장은 상속, 구성 또는 인터페이스 구현의 교체를 통해 달성됩니다.
Bertrand Meyer는 그의 저서 Object-Oriented Software Construction(1988)에서 처음으로 상속을 통해 OCP를 설명했습니다: 기본 클래스는 변경되지 않은 상태로 유지되고 하위 클래스가 그 동작을 확장합니다. Robert Martin이 제안한 OCP의 현대적 해석은 다형성과 인터페이스에 기반합니다: 상속 대신 추상 계약이 사용됩니다.
접근 방식 간의 차이는 상당합니다. 상속은 기본 클래스와 파생 클래스 사이에 강한 결합을 만듭니다. 인터페이스와 구성은 유연성을 제공합니다: 클라이언트 코드를 변경하지 않고 구현을 교체할 수 있습니다. 현대적 OCP는 상속이 아닌 추상화에 관한 것입니다.
다형적 OCP는 계약을 정의하기 위해 추상 클래스나 인터페이스를 사용합니다. 클라이언트 코드는 구체적인 구현을 알지 못한 채 추상화와 함께 작동합니다. 새로운 기능은 동일한 인터페이스를 구현하는 새 클래스를 생성하여 추가됩니다 — 기존 코드를 단 한 줄도 변경하지 않고. 이는 시스템을 변경에 강하게 만들고 확장에 대해 예측 가능하게 만듭니다.
모바일 개발에서 이 접근 방식은 널리 사용됩니다: Strategy 패턴은 단일 인터페이스를 통해 알고리즘(이미지 압축, 캐싱, 인증)을 교체할 수 있게 합니다. 새 전략을 추가해도 그것을 사용하는 코드를 변경할 필요가 없습니다.
OCP 구현은 변경 가능한 동작을 추상화로 분리하는 것부터 시작됩니다. 코드에 switch 구성이나 if-else 체인이 객체의 유형을 검사하는 경우 — 이것은 OCP를 적용하라는 신호입니다. 각 조건 분기는 확장 시 새 분기를 추가해야 할 가능성이 있습니다.
OCP에 따른 리팩토링 프로세스는 세 단계로 구성됩니다: 변경 가능한 측면(확장할 수 있는 것)을 식별하고, 이를 인터페이스나 추상 클래스로 분리하고, 클라이언트 코드를 구체적인 클래스 대신 추상화와 함께 작동하도록 다시 작성합니다. 이후에는 클라이언트를 변경하지 않고 새 기능이 추가됩니다.
중요한 설명: 수정에 대한 폐쇄성은 절대적이지 않습니다. 요구사항 변경이 추상화 자체나 계약에 영향을 미치는 경우 — 변경은 불가피합니다. OCP는 계약이 아닌 구현의 변경으로부터 보호합니다. 좋은 설계는 계약이 안정적이고 구현이 가변적이라고 가정합니다.
아키텍처의 OCP 호환성을 평가할 때는 확장 지점을 살펴보는 것이 유용합니다. 개발자가 새 유형에 대해 if-else나 switch를 추가하는 각 지점은 추상화의 후보입니다. OCP에 따라 설계된 시스템은 예측 가능한 확장 지점을 가집니다: “새 유형을 추가하려면 이 인터페이스를 구현하십시오”라는 문서와 함께 제공되는 인터페이스입니다. Android에서는 ViewModelProvider.Factory와 함께 Factory 패턴이 명확한 예입니다 — 새 ViewModel 유형을 추가해도 기존 팩토리를 변경할 필요가 없습니다.
가장 효과적인 패턴 모바일 개발에서 OCP를 준수하기 위한 것으로 Strategy, Template Method, Decorator 및 Factory가 포함됩니다. 각각은 다양한 객체 지향 설계 메커니즘을 통해 기존 코드를 수정하지 않고 동작을 확장하는 문제를 해결합니다.
Strategy는 공통 인터페이스를 통해 알고리즘을 즉시 교체할 수 있게 합니다. iOS 개발에서는 애니메이션 및 양식 유효성 검사에 전략이 사용됩니다. Template Method는 기본 클래스에서 알고리즘의 뼈대를 정의하고 하위 클래스가 단계를 재정의합니다 — 공통 구조를 가지지만 내용이 다른 화면에 적합합니다.
Decorator는 객체의 클래스를 변경하지 않고 동적으로 동작을 추가합니다. Android에서는 Decorator가 Repository를 캐싱 또는 로깅 계층으로 래핑하는 데 사용됩니다. Factory Method는 인터페이스를 통해 객체를 생성하여 하위 클래스가 인스턴스화할 클래스를 결정할 수 있게 합니다 — OCP 호환 의존성 생성의 기초입니다.
패턴 선택은 확장되는 동작의 안정성에 따라 달라집니다. Strategy는 알고리즘이 완전히 교체될 때 최적입니다. Template Method — 구조는 고정되어 있지만 단계가 가변적일 때. Decorator — 확장이 클라이언트에게 투명해야 할 때. Android 및 iOS의 대부분 시나리오에서는 Strategy + 의존성 주입으로 충분합니다.
OCP 없이 이러한 패턴을 적용하는 것은 기술적으로 가능하지만 의미를 잃습니다. 왜 추가적인 추상화 수준을 도입하는지를 정당화하는 것은 OCP입니다: 기존 코드를 다시 작성하지 않고 시스템이 성장할 수 있도록 하기 위해서입니다.
결제 처리가 포함된 Android 예제를 살펴보겠습니다. OCP가 없으면 각 새 결제 시스템마다 핸들러 클래스의 변경이 필요합니다. OCP를 사용하면 기존 코드를 수정하지 않고 새 인터페이스 구현이 추가됩니다.
// OCP 위반: 새 시스템 추가 시 switch 수정 필요
class BadPaymentProcessor {
fun process(type: String) {
when (type) {
"card" -> // 카드 처리
"paypal" -> // PayPal 처리
}
}
}
// OCP 호환 설계
interface PaymentMethod {
fun pay(amount: Double)
}
class CardPayment : PaymentMethod {
override fun pay(amount: Double) { }
}
class PayPalPayment : PaymentMethod {
override fun pay(amount: Double) { }
}
// 새 시스템 — 새 클래스, 기존 코드 변경 없이
class ApplePayPayment : PaymentMethod {
override fun pay(amount: Double) { }
}
텍스트 필드 유효성 검사가 포함된 iOS 예제는 Swift 프로토콜을 통해 동일한 로직을 보여줍니다:
// OCP 호환 유효성 검사
protocol ValidationRule {
func validate(_ input: String) -> Bool
}
struct EmailRule: ValidationRule {
func validate(_ input: String) -> Bool {
return input.contains("@")
}
}
struct PhoneRule: ValidationRule {
func validate(_ input: String) -> Bool {
return input.count == 11
}
}
// 새 규칙을 추가해도 유효성 검사기 코드 변경 불필요
struct PasswordRule: ValidationRule {
func validate(_ input: String) -> Bool {
return input.count >= 8
}
}
이 예제에서 OCP의 주요 이점: ApplePay 또는 PasswordRule을 추가해도 기존 클래스를 수정할 필요가 없습니다. 코드는 수평으로 확장됩니다 — 이전 파일을 변경하는 대신 새 파일을 통해. 이는 회귀 위험을 줄이고 새 기능 구현을 가속화합니다.
가장 흔한 위반은 객체 유형에 기반한 switch 또는 when 구성입니다. 새 유형이 추가될 때마다 코드에서 해당 switch를 모두 찾아 새 분기를 추가해야 합니다. 놓친 switch는 컴파일 시간에 발견하기 어려운 런타임 버그입니다.
모바일 개발에서는 열거형 값에 의존하는 메서드를 가진 거대한 enum 클래스를 사용할 때 OCP가 위반됩니다. 새 enum 요소를 추가하려면 프로젝트 전체의 모든 switch를 변경해야 합니다. 대안은 인터페이스를 통한 다형성으로, 각 유형이 자신의 동작을 구현합니다.
또 다른 일반적인 위반은 God Adapter입니다: if-else를 통해 다양한 셀 유형을 처리하는 RecyclerView.Adapter(Android) 또는 UITableViewDataSource(iOS). 각 새 셀 유형은 어댑터의 확장을 필요로 합니다. 해결책은 공통 bind 메서드를 가진 다형적 ViewHolder로, 각 셀 유형이 자신의 렌더링을 담당합니다.
예방 조치에는 다음이 포함됩니다: 다형성을 선호하여 유형 기반 switch를 피하고, 인터페이스를 통해 의존성을 주입하고, 구성에 따라 객체를 생성하기 위해 Factory 패턴을 사용합니다. “유형별 스위치”가 있는지 코드를 분석하는 것은 OCP 지향 팀에서 코드 검토의 필수 부분입니다.
기존 OCP 위반의 리팩토링은 Replace Conditional with Polymorphism을 통해 수행됩니다: 각 조건 분기가 공통 인터페이스를 구현하는 별도의 클래스가 됩니다. 클라이언트 코드는 인터페이스와 함께 작동하도록 다시 작성되고 구체적인 구현은 팩토리 또는 DI 컨테이너를 통해 제공됩니다.
OCP와 다형성이 모든 확장 문제를 해결하는 것은 아님을 이해하는 것이 중요합니다. 아키텍처가 잘못 선택된 경우 새 기능을 추가하려면 구현뿐만 아니라 계약도 변경해야 합니다. 좋은 아키텍처는 확장 방향을 예측하고 정확히 해당 지점에 추상화를 배치합니다. OCP에 대한 투자는 프로젝트의 수명이 길어지고 특정 모듈의 요구사항이 자주 변경될수록 더 큰 효과를 냅니다.
자주 묻는 질문
아니요. OCP는 동일한 추상화와 관련된 새 기능을 추가할 때 기존 코드를 변경하는 것을 금지합니다. 계약 변경, 버그 수정 및 리팩토링은 OCP 위반이 아닙니다 — 이 원칙은 확장 시 연쇄적 변경으로부터 보호합니다.
Strategy는 OCP의 직접적인 구현입니다. 전략 인터페이스가 계약을 정의하고, 클라이언트는 추상화에 의존하며, 구체적인 전략이 가변적 동작을 구현합니다. 새 전략을 추가해도 클라이언트를 변경할 필요가 없습니다 — 이것이 수정에 대한 폐쇄성을 갖춘 확장에 대한 개방성입니다.
네, 상속과 Template Method를 통해 가능합니다: 기본 클래스가 알고리즘의 뼈대를 정의하고 하위 클래스가 단계를 재정의합니다. 그러나 상속은 강한 결합을 만들고 인터페이스보다 유연성이 떨어집니다. 현대 개발에서는 인터페이스와 구성이 OCP를 구현하는 선호되는 방법으로 간주됩니다.
OCP 호환 코드는 테스팅을 단순화합니다: 각 인터페이스 구현이 격리되어 테스트됩니다. 클라이언트 코드는 모의 구현으로 테스트되어 특정 동작에 얽매이지 않고 로직을 검증할 수 있습니다. 시스템 확장 시 기존 테스트를 다시 작성할 필요가 없습니다.
아니요. OCP는 기능 확장이 예측 가능할 때 정당화됩니다. 확장 계획이 없는 안정적인 코드에는 추가 추상화가 과도합니다. YAGNI(You Ain't Gonna Need It)는 OCP에 대한 좋은 균형입니다: 추상화는 두 번째 동작 변형이 나타날 때 도입되며 선제적으로 도입되지 않습니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.