OCP(Open Closed Principle) - 개방-폐쇄 원칙
- 확장에 대해서는 열려있고(open), 수정에 대해서는 닫혀있어야(closed) 한다
- 확장은 새로운 기능이 추가되는 것을 말한다. 추상화를 이용해서 확장을 구현하되, 클래스에 직접적인 수정은 최소화한다
- 기존 기능에 대해 추가 요청이 오면, 확장(interface)으로 쉽게 구현할 수 있어야하고 클래스에 대한 변경은 최소화 되어야한다
- 즉, 기존 코드 객체를 변경하지 않으면서, 기능을 추가할 수 있어야한다
- 추상화 사용을 이용하여 관계 구축을 하는 것을 권장한다는 의미이다(추상클래스, 상속)
- 객체지향의 장점중 하나인 확장/다형성을 이용하라는 뜻
- OCP 개방폐쇄원칙 == 다형성/확장 == 추상화
OCP가 지켜지지 않은 프로그램의 경우 수정요청이 들어왔을 때 객체에(클래스, 모듈, 핵심메서드) 직접적인 수정이 이루어진다. 이러면 유지보수의 비용 증가가 일어난다.(개발자가 전체코드를 확인하고/전체코드 테스트를 돌리고/수정코드 적용후 전체코드를 다시 돌려보고..😩)
OCP는 Solid 다섯가지 원칙중에서도 가장 중요한데, 그 이유는 OCP의 추상화를 지켰더라도 추상화의 레벨이나 상속구조를 제대로 파악하지 못하고 개발한 경우 LSP, ISP 위반으로 이어지기 때문이다. 또한 OCP는 DIP의 설계 기반이 되기도 하기때문에 많은 고민과 경험이 필요하다.
🚫 OCP 위반 사례: CakeMaker 클래스 (직접 수정)
OCP 위반 사례에서 '객체'는 핵심 로직을 가진 CakeMaker 클래스입니다. 이 클래스는 현재 초콜릿 케이크만 만들 수 있습니다.
// OCP를 위반하는 CakeMaker 클래스 (초기)
public class CakeMaker {
public String makeCake(String cakeType) {
if (cakeType.equalsIgnoreCase("Chocolate")) {
return "맛있는 초콜릿 케이크를 만들었습니다.";
} else {
return "지원하지 않는 케이크 종류입니다.";
}
}
}
⚠️ 문제 발생: 딸기 케이크를 추가해야 할 때
새로운 요구사항으로 딸기 케이크를 만들어야 합니다. OCP를 위반하는 방식은 기존 CakeMaker 클래스를 직접 수정하는 것입니다.
// OCP 위반: 딸기 케이크 기능을 위해 기존 클래스(객체)를 직접 수정
public class CakeMaker {
public String makeCake(String cakeType) {
if (cakeType.equalsIgnoreCase("Chocolate")) {
return "맛있는 초콜릿 케이크를 만들었습니다.";
}
// 🚨 기존 코드를 직접 수정(변경)해야 합니다. OCP 위반!
else if (cakeType.equalsIgnoreCase("Strawberry")) {
return "상큼한 딸기 케이크를 만들었습니다.";
}
else {
return "지원하지 않는 케이크 종류입니다.";
}
}
}
➡️ 위반 이유: 새로운 기능(딸기 케이크)을 추가할 때, 기존에 잘 작동하던 CakeMaker.java 파일을 열어 코드를 직접 수정해야 했습니다. 이는 변경에 닫혀있지 않은 설계입니다.
✅ OCP 준수 사례: 책임과 계층 분리
OCP를 준수하기 위해 인터페이스(추상화된 객체)를 사용하여 케이크 제작 과정을 추상화하고, 확장 기능을 위한 새로운 클래스를 추가합니다.
1. 확장의 발판: CakeRecipe 인터페이스 정의
핵심 로직을 추상화하는 인터페이스 객체를 정의하여 확장성을 확보합니다.
// 1. 확장의 발판: 추상화된 객체 (인터페이스) - "확장에 열려있습니다."
public interface CakeRecipe {
String getName();
String prepare(); // 케이크를 만드는 메서드
}
2. 기존 기능 구현: 초콜릿 케이크
기존의 초콜릿 케이크 기능을 이 인터페이스를 구현하는 클래스로 분리합니다.
public class ChocolateCake implements CakeRecipe {
@Override
public String getName() {
return "초콜릿 케이크";
}
@Override
public String prepare() {
return "맛있는 초콜릿 케이크를 만들었습니다.";
}
}
3. OCP 핵심: CakeMaker 객체는 변경에 닫혀있음
CakeMaker는 이제 구체적인 케이크 종류에 의존하지 않고, CakeRecipe 인터페이스에 의존합니다. 이 클래스는 더 이상 수정되지 않습니다!
// 3. 변경에 닫힘: 핵심 객체는 수정되지 않습니다.
public class CakeMaker {
// CakeMaker는 이제 어떤 CakeRecipe 객체가 들어와도 처리할 수 있습니다.
public String makeCake(CakeRecipe recipe) {
// 내부 로직은 단순히 전달받은 객체의 메서드를 호출합니다.
System.out.println("주문하신 " + recipe.getName() + "를 만드는 중...");
return recipe.prepare();
}
}
🍓 새로운 기능 추가: 딸기 케이크 (확장에 열려있음)
이제 새로운 요구사항인 딸기 케이크를 추가해 보겠습니다. OCP를 준수하는 방식은 기존 CakeMaker 클래스를 건드리지 않고 새로운 클래스만 추가하는 것입니다.
4. 확장: 새로운 클래스 StrawberryCake 추가
새로운 기능을 위해 CakeRecipe 인터페이스를 구현하는 새로운 클래스만 추가합니다. 기존 코드는 수정하지 않습니다. (확장에 열려있음)
// 4. 확장: 새로운 기능이 필요하면 새로운 객체(클래스)를 '추가'합니다.
public class StrawberryCake implements CakeRecipe {
@Override
public String getName() {
return "딸기 케이크";
}
@Override
public String prepare() {
return "상큼한 딸기 케이크를 만들었습니다.";
}
}
5. OCP 준수 구조의 최종 사용
CakeMaker 객체(클래스)를 수정하지 않고도 새로운 기능을 완벽하게 처리할 수 있습니다.
// OCP 준수 클래스 최종 사용 예시
public class MainOCP {
public static void main(String[] args) {
CakeMaker maker = new CakeMaker();
// 기존 기능 사용 (ChocolateCake 객체 주입)
CakeRecipe chocolate = new ChocolateCake();
String result1 = maker.makeCake(chocolate);
System.out.println(result1); // 맛있는 초콜릿 케이크를 만들었습니다.
System.out.println("--------------------");
// 🚨 새로운 기능 사용 (StrawberryCake 객체 주입)
// CakeMaker 클래스는 수정되지 않았습니다!
CakeRecipe strawberry = new StrawberryCake();
String result2 = maker.makeCake(strawberry);
System.out.println(result2); // 상큼한 딸기 케이크를 만들었습니다.
}
}
✨ OCP 준수의 이점
- 변경에 닫힘: 새로운 케이크 종류(예: 치즈 케이크, 블루베리 케이크)가 추가되어도 핵심 객체인 CakeMaker 클래스는 수정할 필요가 없습니다. 이는 안정성과 유지보수성을 높입니다.
- 확장에 열림: 새로운 케이크 종류가 필요할 때마다 CakeRecipe 인터페이스를 구현하는 새로운 클래스만 추가하면 되므로, 기능을 쉽게 확장할 수 있습니다.
이처럼 OCP는 추상화(인터페이스/추상 클래스)를 통해 시스템의 유연성을 극대화하여, 미래의 변경 사항에 대비하는 설계 원칙입니다.
(Inpa 블로그참고)OCP원칙을 잘 지켜서 설계한 프로그램이 데이터베이스 인터페이스 JDBC라고 해요. DB연결 종류를 oracle에서 mysql로 변경할때 추가코딩없이 설정파일에서 Connection 객체의 DB종류만 바꿔주면 바로 변경해서 연결이 가능하죠.
OCP의 핵심은 코드가 구체적인 구현이 아닌 추상화된 것(인터페이스나 추상 클래스)에 의존하도록 만드는 것입니다.
🧠 객체 지향에서 추상화(Abstraction)란?
추상화는 단순히 인터페이스나 추상 클래스라는 문법적인 요소만을 의미하지 않습니다.
| 추상화 기법 | 특징 | OCP에서의 역할 |
| 인터페이스 (Interface) | 메서드의 선언만 있고 구현이 없습니다. 다중 구현(다중 상속의 효과)이 가능합니다. | 주로 역할이나 규격을 정의하여, 해당 역할을 수행하는 모든 구체 클래스들이 확장의 통로를 따르도록 합니다. |
| 추상 클래스 (Abstract Class) | 메서드의 선언과 함께 부분적인 구현을 가질 수 있습니다. 단일 상속만 가능합니다. | 공통된 기능과 상태를 묶어두고, 구체적인 세부 구현만 자식 클래스에게 맡겨 변경에 닫히도록 만듭니다. |
추상화의 정의: '필요한 것'에만 집중
추상화는 복잡한 현실 세계의 객체에서 특정 목적에 필요한 핵심적인 특징이나 기능만을 추출하여 모델링하는 과정입니다.
- 본질: 복잡성을 숨기고(은닉), 객체의 핵심적인 역할만을 외부에 드러내는 것입니다.
2-1. 인터페이스 : 자동차의 운전대 🚗
추상화를 가장 쉽게 설명할 수 있는 비유는 자동차의 운전대(스티어링 휠)입니다.
운전자는 자동차 내부의 복잡한 구조(구현)를 알 필요 없이 운전대라는 단순화된 인터페이스(추상화)만으로 자동차를 조작할 수 있습니다. 자동차 제조사가 내부 조향 방식을 바꾸더라도 운전대 모양과 역할만 유지된다면, 운전자는 운전대(객체)를 수정하지 않고 운전할 수 있습니다.
| 요소 | 역할 |
| 운전대 (추상화된 객체) | 운전자가 자동차를 좌우로 조작하는 핵심 역할만을 제공합니다. |
| 내부 복잡성 | 운전대 아래에는 바퀴를 돌리는 수많은 기어, 링크, 유압 장치 등 복잡한 기계 장치들이 있습니다. |
이 이론을 코드로 구현하면 ?
| 자동차 요소 (비유) | 코드 객체 | OCP 역할 | 설명 |
| 운전대 | 인터페이스 (SteeringWheel) | 확장의 발판 | 자동차 조향의 핵심 '규격'을 정의합니다. (e.g., turnLeft(), turnRight()) |
| 자동차 (핵심 객체) | 클래스 (Car) | 변경에 닫힘 | 운전대 인터페이스에만 의존하며, 내부 로직은 수정되지 않습니다. |
| 기어/조향 장치 | 구현 클래스 (HydraulicSteering implements SteeringWheel , ElectricSteering implements SteeringWheel) | 확장 기능 | 운전대 규격을 구현하여 구체적인 조향 방식을 정의합니다. |
2-2. 추상클래스 : 음료 제조 과정의 공통 로직🥤
음료 주문 시스템을 예로, 추상 클래스가 OCP에서 어떤 역할을 하는지 예를 들어보겠습니다. 여기서 핵심 객체는 음료를 만드는 클래스입니다.
1. 확장의 발판: Beverage 인터페이스 (음료 규격)
먼저, 모든 음료가 반드시 가져야 할 규격을 정의합니다. (OCP의 확장 통로)
// 모든 음료가 가져야 할 최소한의 '역할'을 정의합니다.
public interface Beverage {
String getName();
String prepare(); // 음료를 제조하는 메서드
}
2. 템플릿 제공: AbstractCoffee 추상 클래스
모든 커피(Latte, Americano 등)는 물을 끓이고 컵을 준비하는 공통 과정이 있습니다. 이 공통 로직을 추상 클래스에 담아 자식 클래스들이 재사용하게 합니다.
// Beverage 인터페이스를 구현하는 추상 클래스
public abstract class AbstractCoffee implements Beverage {
// ☕ 공통 구현: 모든 커피에 필요한 로직은 여기서 처리합니다.
private void boilWater() {
System.out.println("물을 끓입니다.");
}
private void prepareCup() {
System.out.println("깨끗한 컵을 준비합니다.");
}
// 🎯 최종 구현은 자식 클래스에게 위임 (이 부분만 다름)
// 이 메서드에는 구현부가 없으며, 반드시 자식 클래스가 구현해야 합니다.
protected abstract String addIngredient();
// 인터페이스의 prepare() 메서드를 구현하여 공통 로직을 템플릿처럼 제공합니다.
@Override
public String prepare() {
boilWater(); // 모든 커피가 동일하게 수행 (공통 로직 재사용)
prepareCup(); // 모든 커피가 동일하게 수행 (공통 로직 재사용)
return addIngredient(); // 자식 클래스마다 다르게 수행될 부분 호출
}
}
3. 구체화: Latte 클래스 (실제 구현)
이제 라떼는 복잡한 물 끓이기, 컵 준비 로직을 작성할 필요 없이 AbstractCoffee를 상속받아 addIngredient - 우유 추가만 구현하면 됩니다.
// 추상 클래스를 상속받는 구체 클래스
public class Latte extends AbstractCoffee {
@Override
public String getName() {
return "카페 라떼";
}
// 오직 라떼만의 고유한 로직만 구현합니다.
@Override
protected String addIngredient() {
return "에스프레소와 따뜻한 우유를 추가하여 라떼를 완성했습니다.";
}
}
✨ 추상 클래스의 역할 요약
예시에서 추상 클래스(AbstractCoffee)는 OCP를 구현할 때 다음과 같은 중요한 역할을 합니다.
- 공통 코드 재사용: 물 끓이기(boilWater())나 컵 준비(prepareCup()) 같은 공통된 로직을 한 곳에 모아 중복 코드를 제거하고 자식 클래스들이 쉽게 재사용하게 합니다.
- 템플릿 제공: 전체 제조 과정(prepare())의 뼈대(템플릿)를 정의하고, 변화하는 부분(addIngredient())만 자식에게 맡겨서 일관성을 유지합니다.
- 확장 및 변경에 대한 통제: 새로운 커피(Mocha)를 추가할 때, AbstractCoffee 클래스에 정의된 공통 로직은 변경에 닫혀 있게 됩니다. 우리는 새로운 클래스를 추가(확장)만 하면 됩니다.
따라서 누군가 "추상화란 무엇인가요?"라고 묻는다면 다음과 같이 설명할 수 있습니다.
추상화는 복잡한 내부 구현을 숨기고, 객체의 핵심 역할만을 외부에 드러내는 기법입니다. 코드로 구현할 때는 주로 인터페이스(Interface)나 추상 클래스(Abstract Class)와 같은 문법을 사용합니다. 특히 OCP에서는 이 추상화된 규격에 의존함으로써, 새로운 기능이 추가될 때 기존 코드를 수정하지 않고도 확장할 수 있게 해주는 설계의 발판 역할을 합니다.
참고 자료
💠 객체 지향 설계의 5가지 원칙 - S.O.L.I.D
객체 지향 설계의 5원칙 S.O.L.I.D 모든 코드에서 LSP를 지키기에는 어려움. 리스코프 치환 원칙에 따르면 자식 클래스의 인스턴스가 부모 클래스의 인스턴스를 대신하더라도 의도에 맞게 작동되어
inpa.tistory.com
💠 완벽하게 이해하는 OCP (개방 폐쇄 원칙)
개방 폐쇄 원칙 - OCP (Open Closed Principle) 개방 폐쇄의 원칙(OCP)이란 기존의 코드를 변경하지 않으면서, 기능을 추가할 수 있도록 설계가 되어야 한다는 원칙을 말한다. 보통 OCP를 확장에 대해서는
inpa.tistory.com
'IT > Architecture' 카테고리의 다른 글
| ISP 인터페이스 분리 원칙 | SOLID 설계 원칙 (0) | 2025.10.26 |
|---|---|
| LSP 리스코프 치환 원칙 | SOLID 설계 원칙 (0) | 2025.10.26 |
| SRP 단일 책임 원칙 | SOLID 설계 원칙 (0) | 2025.10.26 |
| SOLID 설계 원칙 (0) | 2025.10.25 |
| 인터페이스란 무엇인가? (0) | 2025.10.25 |