5주차 Unit 6.2 — OCP (개방폐쇄원칙)

Psj·2026년 5월 27일

F-lab

목록 보기
173/240

Unit 6.2 — OCP (개방폐쇄원칙)

F-LAB JAVA · 5주차 · Phase 6 · 객체지향 설계 원칙 (OCP & 전략 패턴)
★ 깊이 파기 — 모든 디자인 패턴의 뿌리


📌 학습 목표

이 Unit을 끝내면 다음을 답할 수 있어야 한다.

  • OCP (개방폐쇄원칙) 의 정의는?
  • "확장에 열림, 변경에 닫힘" 의 의미는?
  • ShipmentDao 의 OCP 만족 확인은?
  • OCP 가 깨지는 코드의 신호 는?
  • if-else / switch 가 OCP 위반 신호인 이유는?
  • OCP 를 100% 지키는 게 가능 한가?
  • OCP 와 SRP 의 연결 은?
  • OCP 와 추상화 의 관계는?
  • OCP 가 디자인 패턴의 뿌리인 이유는?

🎯 핵심 한 문장

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 = 확장(가전 추가)에 열림, 변경(벽 공사)에 닫힘, 추상화(콘센트)로 달성.


🧭 9개 섹션 로드맵

1. OCP의 정의
2. 확장에 열림, 변경에 닫힘
3. ShipmentDao의 OCP 만족
4. OCP가 깨지는 신호
5. if-else/switch 위반
6. OCP 100% 가능한가
7. OCP와 SRP 연결
8. OCP와 추상화 (패턴의 뿌리)
9. 면접 + 자기 점검

1️⃣ OCP의 정의

1.1 정의

OCP (Open-Closed Principle):

  "소프트웨어 개체는
   확장에는 열려있고 (Open),
   변경에는 닫혀있어야 (Closed) 한다"

  - 확장: 새 기능 추가
  - 변경: 기존 코드 수정

1.2 핵심 아이디어

핵심 아이디어:

  새 기능:
    - 새 코드 추가 (확장)
    - 기존 코드 안 건드림 (변경 닫힘)

  → 안정성 + 확장성

1.3 왜 중요

왜 중요:

  기존 코드 변경:
    - 버그 위험
    - 테스트 재실행
    - 영향 범위

  → 변경 최소화
  → 추가로 확장

1.4 SOLID 의 O

SOLID 의 O:

  S: 단일 책임
  O: 개방폐쇄 ← 여기
  L: 리스코프 치환
  I: 인터페이스 분리
  D: 의존 역전

1.5 ILIC 의 맥락

// 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
}

1.6 자기 점검 답변

OCP (개방폐쇄원칙) 의 정의는?

:
1. 정의:

  • 확장 열림
  • 변경 닫힘
  1. 아이디어:

    • 추가로 확장
    • 기존 안 건드림
  2. :

    • 변경 위험 최소
  3. SOLID:

    • O

2️⃣ 확장에 열림, 변경에 닫힘

2.1 확장에 열림

확장에 열림 (Open for Extension):

  새 요구사항:
    - 새 동작 추가 가능
    - 새 클래스/구현

  → 기능 확장 가능

2.2 변경에 닫힘

변경에 닫힘 (Closed for Modification):

  기존 코드:
    - 수정 안 함
    - 안정 유지

  → 기존 영향 X

2.3 둘의 조화

둘의 조화:

  확장 (추가) + 변경 X:
    - 새 기능 = 추가
    - 기존 = 그대로

  어떻게?
    - 추상화 (인터페이스)
    - 다형성

2.4 추상화로 달성

추상화로 달성:

  변하는 부분:
    - 추상화 (인터페이스)
    - 확장점

  새 동작:
    - 인터페이스 구현 (확장)
    - 기존 코드 변경 X (닫힘)

2.5 ILIC 의 맥락

// 확장에 열림 + 변경에 닫힘

// 확장점 (추상화)
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)

2.6 자기 점검 답변

"확장에 열림, 변경에 닫힘" 의 의미는?

:
1. 확장 열림:

  • 새 동작 추가 가능
  1. 변경 닫힘:

    • 기존 수정 X
  2. 조화:

    • 추가 + 그대로
  3. 달성:

    • 추상화

3️⃣ ShipmentDao의 OCP 만족

3.1 만족 확인

ShipmentDao OCP 만족:

  새 DB 지원:
    - 새 ConnectionMaker 구현 (확장 ✅)
    - ShipmentDao 코드 그대로 (변경 X ✅)

  → OCP 만족

3.2 확장 부분

// 확장 (새 DB = 새 구현)
class MySqlConnectionMaker implements ConnectionMaker { /* ... */ }
class OracleConnectionMaker implements ConnectionMaker { /* ... */ }
class PostgreSqlConnectionMaker implements ConnectionMaker { /* ... */ }
// 새 DB 추가 = 새 클래스 (확장)

3.3 닫힌 부분

// 변경 안 됨 (ShipmentDao)
public class ShipmentDao {
    private final ConnectionMaker connectionMaker;
    public void add(Shipment s) throws Exception {
        Connection c = connectionMaker.makeConnection();
        // 새 DB 추가해도 이 코드 그대로
    }
}
// ShipmentDao 는 닫힘 (변경 X)

3.4 검증

검증:

  "새 DB 추가 시 ShipmentDao 수정?"
    - NO (구현체만 추가)
    → OCP 만족

  "확장 가능?"
    - YES (새 구현)
    → 확장 열림

3.5 ILIC 의 맥락

// 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 완벽 만족

3.6 자기 점검 답변

ShipmentDao의 OCP 만족 확인은?

:
1. 만족:

  • 새 구현 (확장)
  • DAO 그대로 (닫힘)
  1. 확장:

    • 새 ConnectionMaker
  2. 닫힘:

    • ShipmentDao 불변
  3. 검증:

    • 수정 없이 추가

4️⃣ OCP가 깨지는 신호

4.1 위반 신호

OCP 위반 신호:

1. if-else / switch (타입 분기)
2. 새 케이스마다 수정
3. instanceof 분기
4. 하드코딩된 타입

4.2 타입 분기

// OCP 위반 — 타입 분기
public Connection getConnection(String dbType) {
    if (dbType.equals("MySQL")) {
        return mysqlConnection();
    } else if (dbType.equals("Oracle")) {
        return oracleConnection();
    }
    // 새 DB → 여기 수정 (OCP 위반)
    throw new IllegalArgumentException();
}

4.3 새 케이스 = 수정

새 케이스 = 수정:

  새 DB 추가:
    - else if 추가
    - 기존 메서드 수정

  → 변경에 열림 (위반)

4.4 instanceof 분기

// 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();   // 다형성, 새 동물 = 새 클래스
}

4.5 ILIC 의 맥락

// ❌ 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)
    }
}

4.6 자기 점검 답변

OCP가 깨지는 코드의 신호는?

:
1. 신호:

  • if-else/switch
  • instanceof
  1. 타입 분기:

    • 타입별 처리
  2. 새 케이스:

    • 수정 필요
  3. 해결:

    • 다형성

5️⃣ if-else/switch 위반

5.1 왜 위반

왜 if-else 위반:

  새 케이스 추가:
    - else if 추가
    - 기존 코드 수정

  → 변경에 열림 (위반)
  → 닫혀야 하는데

5.2 산탄총 수술

산탄총 수술 (Shotgun Surgery):

  타입 분기가 여러 곳:
    - 새 타입 추가 시
    - 모든 분기 수정
    - 누락 위험

  → 코드 냄새

5.3 다형성으로 해결

// 다형성 (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)

5.4 모든 분기가 나쁜가

모든 분기가 나쁜가:

  NO:
    - 안정적 케이스 (안 늘어남)
    - 단순 분기 OK

  YES (위반):
    - 자주 늘어나는 타입
    - 여러 곳 분기

→ 확장 가능성으로 판단

5.5 ILIC 의 맥락

// 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)

5.6 자기 점검 답변

if-else/switch가 OCP 위반 신호인 이유는?

:
1. 왜 위반:

  • 새 케이스 → 수정
  • 변경에 열림
  1. 산탄총 수술:

    • 여러 곳 분기
  2. 해결:

    • 다형성
  3. 모든 분기?:

    • 확장 가능성으로 판단

6️⃣ OCP 100% 가능한가

6.1 100% 불가능

OCP 100% 불가능:

  모든 변경을 예측 X:
    - 미래 요구 모름
    - 모든 곳 추상화 X
    - 과도한 추상화 = 복잡

6.2 전략적 적용

전략적 적용:

  변경 예상 지점:
    - 자주 바뀌는 곳
    - 추상화 (확장점)

  안정적 지점:
    - 그냥 둠

→ 선택적

6.3 과도한 추상화

과도한 추상화 위험:

  모든 곳 인터페이스:
    - 복잡도 ↑
    - 이해 어려움
    - YAGNI 위반

→ 필요한 곳만

6.4 변경 후 적용

변경 후 적용:

  처음엔 단순:
    - 분기 OK

  변경 두 번째:
    - 패턴 인식
    - 추상화 (리팩토링)

→ "Rule of Three"

6.5 ILIC 의 맥락

// 전략적 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% 가 아닌 전략적

6.6 자기 점검 답변

OCP를 100% 지키는 게 가능한가?

:
1. 100% 불가:

  • 모든 변경 예측 X
  1. 전략적:

    • 변경 예상 지점만
  2. 과도한 추상화:

    • 복잡도 (YAGNI)
  3. 변경 후:

    • Rule of Three

7️⃣ OCP와 SRP 연결

7.1 연결

OCP ↔ SRP 연결:

  SRP (책임 분리):
    - 관심사별 분리

  OCP (확장):
    - 변경 지점 추상화

  → SRP 가 OCP 토대

7.2 SRP 선행

SRP 선행:

  책임 분리 (SRP):
    - 변하는 부분 식별
    - 분리

  → 확장점 명확 (OCP)

7.3 함께 작동

함께 작동:

  ConnectionMaker 분리:
    - SRP (연결 책임만)
    - + 인터페이스 (OCP 확장점)

  → 두 원칙 동시

7.4 변경 이유 = 확장점

변경 이유 = 확장점:

  SRP:
    - 변경 이유 하나

  OCP:
    - 변경 이유가 확장점

→ 변경 이유별 추상화

7.5 ILIC 의 맥락

// 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 확장점 명확

7.6 자기 점검 답변

OCP와 SRP의 연결은?

:
1. 연결:

  • SRP 가 OCP 토대
  1. SRP 선행:

    • 책임 분리 → 확장점
  2. 함께:

    • 분리 + 인터페이스
  3. 변경 이유:

    • = 확장점

8️⃣ OCP와 추상화 (패턴의 뿌리)

8.1 추상화로 OCP

추상화로 OCP:

  변하는 부분:
    - 인터페이스/추상클래스
    - 확장점

  → 다형성으로 확장
  → 기존 변경 X

8.2 디자인 패턴의 뿌리

디자인 패턴의 뿌리:

  대부분 패턴 = OCP 달성:
    - 전략: 알고리즘 확장
    - 팩토리: 생성 확장
    - 옵저버: 관찰자 확장
    - 데코레이터: 기능 확장

→ OCP 가 근본 목표

8.3 패턴 = OCP 구현

패턴 = OCP 구현:

  전략 패턴:
    - 전략 인터페이스 (확장점)
    - 새 전략 추가 (확장)
    - 컨텍스트 변경 X (닫힘)

  → OCP 구현 도구

8.4 OCP 가 목표

OCP 가 목표:

  패턴은 수단:
    - OCP 달성 위한
    - 검증된 구조

  목표 = OCP (유연한 확장)

8.5 ILIC 의 맥락

// 패턴들이 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

8.6 자기 점검 답변

OCP가 디자인 패턴의 뿌리인 이유는?

:
1. 추상화로 OCP:

  • 인터페이스 확장점
  1. 패턴의 뿌리:

    • 대부분 OCP 달성
  2. 패턴 = 구현:

    • OCP 구현 도구
  3. 목표:

    • OCP (유연 확장)

9️⃣ 면접 + 자기 점검

9.1 면접 단골 질문 매핑

Q핵심 답변
OCP?확장 열림, 변경 닫힘
확장/변경?추가 가능, 기존 안 건드림
ShipmentDao OCP?새 구현(확장), DAO 불변(닫힘)
위반 신호?if-else/switch
if-else 위반?새 케이스 수정
100% 가능?불가 (전략적)
OCP-SRP?SRP 가 토대
추상화?인터페이스 확장점
패턴 뿌리?대부분 OCP 달성
달성 방법?다형성

9.2 자기 점검 체크리스트

정의

  • 확장 열림, 변경 닫힘

ShipmentDao

  • OCP 만족

위반 신호

  • if-else/switch

100%

  • 불가 (전략적)

SRP 연결

  • 토대

추상화

  • 확장점

패턴 뿌리

  • OCP 목표

9.3 추가 심화 질문

Q1: OCP 와 다형성?

답:

  • 다형성이 OCP 핵심 수단
  • 인터페이스 + 구현
  • 같은 호출, 다른 동작
  • 새 구현 = 확장

Q2: enum 과 OCP?

답:

  • enum 분기 (switch) → 위반 가능
  • enum 에 동작 (추상 메서드) → OCP
  • enum 도 다형성 가능
  • 케이스 추가 시 컴파일러 도움

Q3: OCP 위반이 항상 나쁜가?

답:

  • 항상 X
  • 안정적 케이스는 단순 분기 OK
  • 과도한 추상화가 더 나쁠 수도
  • 변경 가능성으로 판단

Q4: Strategy vs OCP?

답:

  • 전략 패턴 = OCP 구현 방법
  • 알고리즘 확장점
  • OCP 가 원칙, 전략이 패턴
  • 원칙 ⊃ 패턴

Q5: 리스코프 치환 (LSP) 과 OCP?

답:

  • LSP: 서브타입 치환 가능
  • OCP: LSP 가 전제
  • 치환 안 되면 OCP 깨짐
  • LSP 가 OCP 보장

🎯 핵심 요약 — 3줄 정리

1. OCP

  • 확장에는 열려있고, 변경에는 닫혀있어야
  • 새 기능 = 추가 (확장), 기존 코드 = 그대로 (닫힘)

2. 위반 신호와 달성

  • if-else/switch 타입 분기 = 위반 신호
  • 추상화 (인터페이스) + 다형성으로 달성

3. 연결과 뿌리

  • SRP 가 OCP 토대 (책임 분리 → 확장점)
  • OCP 는 대부분 디자인 패턴의 근본 목표 (100% 아닌 전략적)

📚 다음으로...

Unit 6.3 — 전략 패턴

이번 Unit에서 OCP 를 봤다면, 다음은 전략 패턴 (Phase 6 마지막).

  • 알고리즘을 인터페이스로 분리
  • Context / Strategy / ConcreteStrategy
  • 전략 패턴 = OCP 구현 도구
  • 전략 vs 템플릿 메소드

Phase 6 진행 상황

🌱 Phase 6 — OCP & 전략 패턴
  ✅ Unit 6.1 인터페이스로 결합도 낮추기
  ✅ Unit 6.2 OCP (개방폐쇄원칙) ★깊이 ← 여기
  ⏭ Unit 6.3 전략 패턴

5주차 누적 진행

✅ 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 완료

profile
Software Developer

0개의 댓글