F-LAB JAVA · 5주차 · Phase 6 · 객체지향 설계 원칙 (OCP & 전략 패턴)
★ 깊이 파기 — 모든 디자인 패턴의 뿌리
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
OCP (Open-Closed Principle) 는 "확장에는 열려있고 변경에는 닫혀있어야 한다" 는 원칙으로, 새 기능을 추가할 때 (확장) 기존 코드를 수정하지 않고 (변경 닫힘) 새 코드만 추가하도록 설계하라는 것이다.
ShipmentDao 는 새 DB 를 지원할 때 ConnectionMaker 구현체를 추가 하기만 하면 되고 (확장에 열림), ShipmentDao 코드는 전혀 수정하지 않으므로 (변경에 닫힘) OCP 를 만족한다.
OCP 가 깨지는 대표적 신호는 if-else 나 switch 문 으로,if (type.equals("MySQL")) ... else if (type.equals("Oracle"))처럼 새 케이스가 추가될 때마다 그 분기 코드를 수정해야 한다면 OCP 위반이다.
현실적으로 OCP 를 100% 지키는 것은 불가능하며, 변경이 자주 일어날 것으로 예상되는 지점 을 식별해 그 부분만 추상화 (인터페이스) 로 확장점을 열어두는 것이 핵심이다.
OCP 는 추상화 (인터페이스) 를 통해 달성되며, SRP (책임 분리) 가 선행되어야 확장점을 명확히 만들 수 있어 서로 연결되고, 사실상 모든 디자인 패턴의 근본 목표다.
OCP = 콘센트 표준:
확장에 열림:
- 새 가전 (선풍기, 청소기, 충전기)
- 콘센트에 그냥 꽂음 (추가)
- 새 가전 = 확장
변경에 닫힘:
- 콘센트(벽) 는 안 바꿈
- 새 가전마다 벽 뚫지 X
- 콘센트 = 변경 닫힘
OCP 위반 신호 (if-else):
- "선풍기면 A 단자, 청소기면 B 단자..."
- 새 가전마다 벽 공사 (수정)
- → 콘센트 표준이 없는 셈
100% 불가:
- 모든 곳에 콘센트 X
- 자주 쓸 곳만 콘센트
- 변경 예상 지점만 확장점
콘센트(인터페이스) = 확장점
가전(구현체) = 확장
→ OCP = 확장(가전 추가)에 열림, 변경(벽 공사)에 닫힘, 추상화(콘센트)로 달성.
1. OCP의 정의
2. 확장에 열림, 변경에 닫힘
3. ShipmentDao의 OCP 만족
4. OCP가 깨지는 신호
5. if-else/switch 위반
6. OCP 100% 가능한가
7. OCP와 SRP 연결
8. OCP와 추상화 (패턴의 뿌리)
9. 면접 + 자기 점검
OCP (Open-Closed Principle):
"소프트웨어 개체는
확장에는 열려있고 (Open),
변경에는 닫혀있어야 (Closed) 한다"
- 확장: 새 기능 추가
- 변경: 기존 코드 수정
핵심 아이디어:
새 기능:
- 새 코드 추가 (확장)
- 기존 코드 안 건드림 (변경 닫힘)
→ 안정성 + 확장성
왜 중요:
기존 코드 변경:
- 버그 위험
- 테스트 재실행
- 영향 범위
→ 변경 최소화
→ 추가로 확장
SOLID 의 O:
S: 단일 책임
O: 개방폐쇄 ← 여기
L: 리스코프 치환
I: 인터페이스 분리
D: 의존 역전
// OCP 만족 설계
public interface ConnectionMaker {
Connection makeConnection() throws Exception;
}
public class ShipmentDao {
private final ConnectionMaker connectionMaker;
public ShipmentDao(ConnectionMaker cm) { this.connectionMaker = cm; }
// 확장에 열림: 새 ConnectionMaker 구현 추가 가능
// 변경에 닫힘: ShipmentDao 코드는 수정 X
}
OCP (개방폐쇄원칙) 의 정의는?
답:
1. 정의:
아이디어:
왜:
SOLID:
확장에 열림 (Open for Extension):
새 요구사항:
- 새 동작 추가 가능
- 새 클래스/구현
→ 기능 확장 가능
변경에 닫힘 (Closed for Modification):
기존 코드:
- 수정 안 함
- 안정 유지
→ 기존 영향 X
둘의 조화:
확장 (추가) + 변경 X:
- 새 기능 = 추가
- 기존 = 그대로
어떻게?
- 추상화 (인터페이스)
- 다형성
추상화로 달성:
변하는 부분:
- 추상화 (인터페이스)
- 확장점
새 동작:
- 인터페이스 구현 (확장)
- 기존 코드 변경 X (닫힘)
// 확장에 열림 + 변경에 닫힘
// 확장점 (추상화)
public interface ConnectionMaker {
Connection makeConnection() throws Exception;
}
// 기존 (변경에 닫힘)
public class ShipmentDao {
private final ConnectionMaker connectionMaker;
public ShipmentDao(ConnectionMaker cm) { this.connectionMaker = cm; }
public void add(Shipment s) throws Exception {
Connection c = connectionMaker.makeConnection();
// 이 코드는 절대 안 바뀜 (닫힘)
}
}
// 확장 (열림) — 새 구현 추가
public class NewDbConnectionMaker implements ConnectionMaker { // 확장
public Connection makeConnection() throws Exception {
return null; // 새 DB
}
}
// ShipmentDao 변경 0줄, 새 구현만 (OCP)
"확장에 열림, 변경에 닫힘" 의 의미는?
답:
1. 확장 열림:
변경 닫힘:
조화:
달성:
ShipmentDao OCP 만족:
새 DB 지원:
- 새 ConnectionMaker 구현 (확장 ✅)
- ShipmentDao 코드 그대로 (변경 X ✅)
→ OCP 만족
// 확장 (새 DB = 새 구현)
class MySqlConnectionMaker implements ConnectionMaker { /* ... */ }
class OracleConnectionMaker implements ConnectionMaker { /* ... */ }
class PostgreSqlConnectionMaker implements ConnectionMaker { /* ... */ }
// 새 DB 추가 = 새 클래스 (확장)
// 변경 안 됨 (ShipmentDao)
public class ShipmentDao {
private final ConnectionMaker connectionMaker;
public void add(Shipment s) throws Exception {
Connection c = connectionMaker.makeConnection();
// 새 DB 추가해도 이 코드 그대로
}
}
// ShipmentDao 는 닫힘 (변경 X)
검증:
"새 DB 추가 시 ShipmentDao 수정?"
- NO (구현체만 추가)
→ OCP 만족
"확장 가능?"
- YES (새 구현)
→ 확장 열림
// OCP 만족 검증
public interface ConnectionMaker {
Connection makeConnection() throws Exception;
}
public class ShipmentDao { // 변경에 닫힘
private final ConnectionMaker connectionMaker;
public ShipmentDao(ConnectionMaker cm) { this.connectionMaker = cm; }
public void add(Shipment s) throws Exception {
Connection c = connectionMaker.makeConnection();
}
}
// 시나리오: 새 고객사 (새 DB) 5개 추가
class CustomerC implements ConnectionMaker { public Connection makeConnection() { return null; } }
class CustomerD implements ConnectionMaker { public Connection makeConnection() { return null; } }
class CustomerE implements ConnectionMaker { public Connection makeConnection() { return null; } }
// → 5개 구현 추가 (확장)
// → ShipmentDao 수정 0줄 (닫힘)
// → OCP 완벽 만족
ShipmentDao의 OCP 만족 확인은?
답:
1. 만족:
확장:
닫힘:
검증:
OCP 위반 신호:
1. if-else / switch (타입 분기)
2. 새 케이스마다 수정
3. instanceof 분기
4. 하드코딩된 타입
// OCP 위반 — 타입 분기
public Connection getConnection(String dbType) {
if (dbType.equals("MySQL")) {
return mysqlConnection();
} else if (dbType.equals("Oracle")) {
return oracleConnection();
}
// 새 DB → 여기 수정 (OCP 위반)
throw new IllegalArgumentException();
}
새 케이스 = 수정:
새 DB 추가:
- else if 추가
- 기존 메서드 수정
→ 변경에 열림 (위반)
// instanceof 분기 (위반 신호)
public void process(Animal animal) {
if (animal instanceof Dog) {
((Dog) animal).bark();
} else if (animal instanceof Cat) {
((Cat) animal).meow();
}
// 새 동물 → 수정 (위반)
}
// ✓ 다형성 (OCP)
public void process(Animal animal) {
animal.makeSound(); // 다형성, 새 동물 = 새 클래스
}
// ❌ OCP 위반 (분기)
public class ShipmentProcessorBad {
public Connection getConnection(String dbType) throws Exception {
if (dbType.equals("customerA")) {
return DriverManager.getConnection("jdbc:mysql://A/ilic", "a", "a");
} else if (dbType.equals("customerB")) {
return DriverManager.getConnection("jdbc:oracle:thin:@B:1521:ilic", "b", "b");
}
// 새 고객사 → 여기 수정 (OCP 위반)
throw new IllegalArgumentException();
}
}
// ✓ OCP 준수 (다형성)
public class ShipmentDao {
private final ConnectionMaker connectionMaker;
public ShipmentDao(ConnectionMaker cm) { this.connectionMaker = cm; }
public void add(Shipment s) throws Exception {
Connection c = connectionMaker.makeConnection(); // 다형성
// 새 고객사 = 새 구현 (분기 X)
}
}
OCP가 깨지는 코드의 신호는?
답:
1. 신호:
타입 분기:
새 케이스:
해결:
왜 if-else 위반:
새 케이스 추가:
- else if 추가
- 기존 코드 수정
→ 변경에 열림 (위반)
→ 닫혀야 하는데
산탄총 수술 (Shotgun Surgery):
타입 분기가 여러 곳:
- 새 타입 추가 시
- 모든 분기 수정
- 누락 위험
→ 코드 냄새
// 다형성 (OCP 준수)
interface ConnectionMaker {
Connection makeConnection() throws Exception;
}
// 새 DB = 새 구현 (분기 X)
class NewDbMaker implements ConnectionMaker {
public Connection makeConnection() { return null; }
}
// 사용 (분기 없음)
Connection c = connectionMaker.makeConnection(); // 다형성
// 어떤 구현이든 (if-else X)
모든 분기가 나쁜가:
NO:
- 안정적 케이스 (안 늘어남)
- 단순 분기 OK
YES (위반):
- 자주 늘어나는 타입
- 여러 곳 분기
→ 확장 가능성으로 판단
// if-else 위반 → 다형성 개선
// ❌ 위반 (운임 계산 타입 분기)
public BigDecimal calculate(Shipment s, String type) {
if (type.equals("SEA")) {
return s.getWeight().multiply(BigDecimal.valueOf(10));
} else if (type.equals("AIR")) {
return s.getWeight().multiply(BigDecimal.valueOf(50));
} else if (type.equals("GROUND")) {
return s.getWeight().multiply(BigDecimal.valueOf(5));
}
// 새 운송 수단 → 수정 (위반)
throw new IllegalArgumentException();
}
// ✓ 다형성 (OCP)
interface FreightCalculator {
BigDecimal calculate(Shipment s);
}
class SeaFreightCalculator implements FreightCalculator {
public BigDecimal calculate(Shipment s) {
return s.getWeight().multiply(BigDecimal.valueOf(10));
}
}
class AirFreightCalculator implements FreightCalculator {
public BigDecimal calculate(Shipment s) {
return s.getWeight().multiply(BigDecimal.valueOf(50));
}
}
// 새 운송 = 새 구현 (분기 X)
if-else/switch가 OCP 위반 신호인 이유는?
답:
1. 왜 위반:
산탄총 수술:
해결:
모든 분기?:
OCP 100% 불가능:
모든 변경을 예측 X:
- 미래 요구 모름
- 모든 곳 추상화 X
- 과도한 추상화 = 복잡
전략적 적용:
변경 예상 지점:
- 자주 바뀌는 곳
- 추상화 (확장점)
안정적 지점:
- 그냥 둠
→ 선택적
과도한 추상화 위험:
모든 곳 인터페이스:
- 복잡도 ↑
- 이해 어려움
- YAGNI 위반
→ 필요한 곳만
변경 후 적용:
처음엔 단순:
- 분기 OK
변경 두 번째:
- 패턴 인식
- 추상화 (리팩토링)
→ "Rule of Three"
// 전략적 OCP 적용
// 자주 바뀌는 곳 → 추상화 (OCP)
public interface ConnectionMaker { // DB 자주 바뀜 → 확장점
Connection makeConnection() throws Exception;
}
public interface FreightCalculator { // 운임 정책 자주 → 확장점
BigDecimal calculate(Shipment s);
}
// 안정적인 곳 → 그냥 (추상화 X)
public class Shipment { // 도메인 모델 (안정)
private Long id;
private String blNo;
private BigDecimal weight;
// getter/setter — 추상화 불필요
}
// 판단:
// - 변경 잦음 (DB, 운임) → OCP 적용
// - 안정 (도메인) → 단순하게
// → 100% 가 아닌 전략적
OCP를 100% 지키는 게 가능한가?
답:
1. 100% 불가:
전략적:
과도한 추상화:
변경 후:
OCP ↔ SRP 연결:
SRP (책임 분리):
- 관심사별 분리
OCP (확장):
- 변경 지점 추상화
→ SRP 가 OCP 토대
SRP 선행:
책임 분리 (SRP):
- 변하는 부분 식별
- 분리
→ 확장점 명확 (OCP)
함께 작동:
ConnectionMaker 분리:
- SRP (연결 책임만)
- + 인터페이스 (OCP 확장점)
→ 두 원칙 동시
변경 이유 = 확장점:
SRP:
- 변경 이유 하나
OCP:
- 변경 이유가 확장점
→ 변경 이유별 추상화
// SRP + OCP 함께
// SRP: 연결 책임 분리
// OCP: 인터페이스로 확장점
public interface ConnectionMaker { // SRP (연결) + OCP (확장점)
Connection makeConnection() throws Exception;
}
// SRP: 운임 계산 책임 분리
// OCP: 인터페이스로 확장점
public interface FreightCalculator { // SRP (계산) + OCP (확장점)
BigDecimal calculate(Shipment s);
}
// ShipmentDao
public class ShipmentDao {
private final ConnectionMaker connectionMaker; // SRP 분리 + OCP
private final FreightCalculator freightCalculator; // SRP 분리 + OCP
// 각 책임 분리(SRP) + 확장점(OCP)
public ShipmentDao(ConnectionMaker cm, FreightCalculator fc) {
this.connectionMaker = cm;
this.freightCalculator = fc;
}
}
// SRP 로 책임 나누니 → OCP 확장점 명확
OCP와 SRP의 연결은?
답:
1. 연결:
SRP 선행:
함께:
변경 이유:
추상화로 OCP:
변하는 부분:
- 인터페이스/추상클래스
- 확장점
→ 다형성으로 확장
→ 기존 변경 X
디자인 패턴의 뿌리:
대부분 패턴 = OCP 달성:
- 전략: 알고리즘 확장
- 팩토리: 생성 확장
- 옵저버: 관찰자 확장
- 데코레이터: 기능 확장
→ OCP 가 근본 목표
패턴 = OCP 구현:
전략 패턴:
- 전략 인터페이스 (확장점)
- 새 전략 추가 (확장)
- 컨텍스트 변경 X (닫힘)
→ OCP 구현 도구
OCP 가 목표:
패턴은 수단:
- OCP 달성 위한
- 검증된 구조
목표 = OCP (유연한 확장)
// 패턴들이 OCP 달성
// 전략 패턴 (OCP)
interface FreightStrategy { // 확장점
BigDecimal calculate(Shipment s);
}
// 새 전략 추가 (확장), 사용처 변경 X (닫힘)
// 팩토리 메소드 (OCP)
interface ConnectionMaker { // 확장점
Connection makeConnection() throws Exception;
}
// 새 구현 추가 (확장)
// 옵저버 (OCP)
interface ShipmentListener { // 확장점
void onStatusChanged(Shipment s);
}
// 새 리스너 추가 (확장)
// 공통: 모두 OCP 달성 (확장점 = 인터페이스)
// → 패턴의 뿌리 = OCP
OCP가 디자인 패턴의 뿌리인 이유는?
답:
1. 추상화로 OCP:
패턴의 뿌리:
패턴 = 구현:
목표:
| Q | 핵심 답변 |
|---|---|
| OCP? | 확장 열림, 변경 닫힘 |
| 확장/변경? | 추가 가능, 기존 안 건드림 |
| ShipmentDao OCP? | 새 구현(확장), DAO 불변(닫힘) |
| 위반 신호? | if-else/switch |
| if-else 위반? | 새 케이스 수정 |
| 100% 가능? | 불가 (전략적) |
| OCP-SRP? | SRP 가 토대 |
| 추상화? | 인터페이스 확장점 |
| 패턴 뿌리? | 대부분 OCP 달성 |
| 달성 방법? | 다형성 |
답:
답:
답:
답:
답:
1. OCP
2. 위반 신호와 달성
3. 연결과 뿌리
이번 Unit에서 OCP 를 봤다면, 다음은 전략 패턴 (Phase 6 마지막).
🌱 Phase 6 — OCP & 전략 패턴
✅ Unit 6.1 인터페이스로 결합도 낮추기
✅ Unit 6.2 OCP (개방폐쇄원칙) ★깊이 ← 여기
⏭ Unit 6.3 전략 패턴
✅ Part A — 동시성 마무리 (7 Unit)
🌱 Part B — 토비의 스프링
✅ Phase 3~5 (9 Unit)
🌱 Phase 6 — OCP & 전략 패턴 (2/3 진행)
총: 18/26 Unit
F-LAB JAVA · 5주차 · Phase 6 · Unit 6.2 · 끝
★ 깊이 파기 — OCP 완료