🎯1주차 Unit 3.2 — OCP (개방-폐쇄 원칙)

Psj·2026년 5월 7일

F-lab

목록 보기
31/142

🎯 Unit 3.2 — OCP (개방-폐쇄 원칙) ★★★

F-lab Java 1주차 / Phase 3 / Unit 3.2 본격 학습 자료
9-섹션 마스터 프롬프트 형식으로 깊이 파헤친다.

선수 지식: Unit 2.4 (다형성), Unit 3.1 (SRP)
다음 Unit: 3.3 — LSP (리스코프 치환 원칙)

이 Unit의 의미: 다형성(Unit 2.4) 의 직접적 결과.
SOLID에서 가장 강력한 원칙이자, 현대 소프트웨어 아키텍처의 토대.
Strategy 패턴, Spring DI, JPA, 모든 게 OCP 위에 서 있다.


🌍 1. 세상 속 비유

OCP = "콘센트 표준"

집에 콘센트가 있습니다. 220V 표준 규격이라 모든 가전제품이 호환:

  • 냉장고 → 콘센트에 꽂으면 작동
  • 청소기 → 콘센트에 꽂으면 작동
  • 노트북 충전기 → 콘센트에 꽂으면 작동

새 가전제품이 나오면? 콘센트만 표준대로 만들면 끝:

  • 콘센트(집) 수정 X
  • 새 제품 추가만

만약 콘센트에 표준이 없다면?

  • 새 가전 살 때마다 콘센트 교체
  • 벽 뜯어 공사
  • 악몽

핵심:

  • 콘센트 = 변경에는 닫혀있음 (수정 안 함)
  • 새 제품 = 확장에는 열려있음 (추가 자유)

이게 OCP의 정신.


더 일상적인 비유 — "스마트폰 앱"

스마트폰의 운영체제(iOS, Android)는 새 앱이 추가될 때마다 OS를 수정하지 않습니다:

  • 새 앱 설치 → OS는 그대로
  • 앱이 정해진 API (인터페이스) 만 따르면 됨

만약 새 앱 추가할 때마다 OS를 업데이트해야 한다면?

  • 앱 1만 개 → OS 1만 번 업데이트
  • 모든 폰 사용자에게 영향
  • 악몽

OS의 비밀:

  • 명확한 API (인터페이스) 정의
  • 앱은 그 API를 따름
  • OS는 닫혀있고, 앱은 열려있음

OCP가 곧 플랫폼 사고방식.


핵심 한 문장

"기존 코드를 수정하지 않고 새 기능을 추가할 수 있어야 한다."

OCP의 두 가지 측면:

  • 확장에 열려있음 (Open for Extension): 새 기능 추가 가능
  • 변경에 닫혀있음 (Closed for Modification): 기존 코드는 그대로

비유 정리:

비유 요소OCP 적용
콘센트 표준인터페이스/추상 클래스
새 가전제품새 구현체 (확장)
콘센트 수정 X기존 코드 변경 X
표준 미준수OCP 위반

🔥 2. 탄생 배경

Bertrand Meyer (1988) — OCP의 원조

OCP 정의:

"Software entities should be open for extension, but closed for modification."
("소프트웨어는 확장에 열려있고, 수정에 닫혀있어야 한다.")

Bertrand Meyer, Object-Oriented Software Construction (1988) 에서 정립.

그 후: Robert C. Martin (Uncle Bob) 이 SOLID에 포함시키며 대중화.


등장 배경 — "수정의 비용은 비싸다"

1980-90년대, 거대 시스템들이 등장하면서 발견된 현상:

한 번 운영 환경에 배포된 코드를 수정하는 비용:

  • 코드 수정 자체 (적음)
  • 다른 부분에 미치는 영향 분석 (큼)
  • 모든 테스트 재실행 (큼)
  • 운영 배포 + 검증 (큼)
  • 버그 발생 시 롤백 (매우 큼)

수정 = 위험 + 비용

해결 아이디어:

"그냥 수정 안 하면 안 되나?"

조건:

  • 새 요구사항이 와도 기존 코드는 손대지 않기
  • 새 코드만 추가
  • 위험 최소화, 비용 절감

다형성과 OCP의 관계 ⭐ (Unit 2.4 와 통합)

OCP가 가능한 이유 = 다형성 덕분.

[Unit 2.4: 다형성]
  ↓ "같은 메시지에 객체마다 다른 응답"
  ↓
[OCP의 토대]
  ↓ "새 객체를 추가해도 기존 호출자는 그대로"
  ↓
[OCP 달성 ✅]

핵심 통찰:

"OCP는 다형성을 응용한 설계 원칙이다"

다형성 없는 OCP는 불가능. 두 개념은 하나의 사고.


자바 생태계와 OCP

자바 생태계가 거대하게 성장한 이유 중 하나가 OCP:

Spring:

  • 새 빈 추가 → 기존 코드 수정 X
  • 인터페이스 기반 DI

JPA:

  • 새 Entity 추가 → 프레임워크 수정 X
  • 표준 어노테이션 활용

JDBC:

  • 새 DB 드라이버 → JDBC 코드 수정 X
  • DriverManager가 표준

거의 모든 자바 프레임워크 = OCP의 결과.


핵심 통찰

"OCP는 '변화를 예상하고 미리 설계하는' 원칙이다."

새 기능이 추가될 때마다 기존 코드를 수정한다면, 시스템은 수정의 무게에 짓눌려 결국 동작 안 함. 확장 지점을 미리 만들어둠 으로써 변화를 자연스럽게 흡수할 수 있다.

그러나 모든 변화를 예측할 수는 없으니, 실제로 변화가 일어나는 영역에만 OCP를 적용하는 균형이 필요.


💣 3. 없으면 생기는 문제

OCP를 위반했을 때의 구체적 문제를 ILIC 시나리오로 보겠습니다.

시나리오: ILIC 운임 등급별 처리

새 고객 등급이 추가될 때마다 처리 로직이 늘어나는 시스템.


OCP 위반 — 매번 기존 코드 수정 ❌

public class FareService {
    
    public int calculateDiscount(Customer customer, int amount) {
        // 등급별 if 분기
        if (customer.getLevel().equals("NORMAL")) {
            return 0;
        } else if (customer.getLevel().equals("VIP")) {
            return amount * 20 / 100;
        } else if (customer.getLevel().equals("PARTNER")) {
            return amount * 30 / 100;
        }
        return 0;
    }
    
    public String getNotificationGreeting(Customer customer) {
        if (customer.getLevel().equals("NORMAL")) return "안녕하세요";
        else if (customer.getLevel().equals("VIP")) return "VIP 고객님";
        else if (customer.getLevel().equals("PARTNER")) return "파트너님";
        return "고객님";
    }
    
    public boolean canApplyExtraBenefit(Customer customer) {
        if (customer.getLevel().equals("VIP")) return true;
        if (customer.getLevel().equals("PARTNER")) return true;
        return false;
    }
    
    public double getPointAccumulationRate(Customer customer) {
        if (customer.getLevel().equals("VIP")) return 0.05;
        if (customer.getLevel().equals("PARTNER")) return 0.10;
        return 0.01;
    }
}

새 등급 (PLATINUM) 추가 시 — 모든 메서드 수정 ❌

public class FareService {
    
    public int calculateDiscount(Customer customer, int amount) {
        // 기존 코드들 ...
        else if (customer.getLevel().equals("PLATINUM")) {  // ← 추가 1
            return amount * 25 / 100;
        }
        return 0;
    }
    
    public String getNotificationGreeting(Customer customer) {
        // 기존 코드들 ...
        else if (customer.getLevel().equals("PLATINUM")) {  // ← 추가 2
            return "플래티넘 고객님";
        }
        return "고객님";
    }
    
    public boolean canApplyExtraBenefit(Customer customer) {
        // 기존 코드들 ...
        if (customer.getLevel().equals("PLATINUM")) return true;  // ← 추가 3
        return false;
    }
    
    public double getPointAccumulationRate(Customer customer) {
        // 기존 코드들 ...
        if (customer.getLevel().equals("PLATINUM")) return 0.08;  // ← 추가 4
        return 0.01;
    }
}

PLATINUM 1개 추가에 4개 메서드 수정


OCP 위반의 4가지 심각한 문제

1. 변경 영향 폭발

FareService 가 100개 메서드를 가지고 있다면 → 100개 메서드 수정.

수정 비용 폭발.


2. 회귀 위험 (Regression)

기존 동작이 깨질 가능성:

// 의도: VIP 분기 추가
else if (customer.getLevel().equals("PLATINUM")) {  // 새 추가
    return amount * 25 / 100;
}
// 실수로 기존 VIP 로직을 건드림 ❌

새 기능 추가 시 기존 기능 깨질 위험.


3. 테스트 폭증

  • 5개 등급 × 4개 메서드 = 20개 테스트 케이스
  • 새 등급 추가 → +4 테스트
  • 테스트 유지 비용 폭증.

4. 협업 충돌

여러 개발자가 동시에 새 등급 추가 시:

  • A: SILVER 등급 추가 (FareService 수정)
  • B: GOLD 등급 추가 (FareService 수정)
  • 머지 충돌.

OCP 적용 — 다형성으로 해결 ✅

// 1. 추상 클래스 — "확장 지점"
public abstract class Customer {
    protected String name;
    protected String email;
    
    // 등급별 다른 행동을 자식이 정의
    public abstract int calculateDiscountRate();
    public abstract String getNotificationGreeting();
    public abstract boolean canApplyExtraBenefit();
    public abstract double getPointAccumulationRate();
    
    // 공통 로직
    public int calculateDiscount(int amount) {
        return amount * calculateDiscountRate() / 100;
    }
}

// 2. 각 등급 — 자기 정책 정의
public class NormalCustomer extends Customer {
    @Override public int calculateDiscountRate() { return 0; }
    @Override public String getNotificationGreeting() { return "안녕하세요"; }
    @Override public boolean canApplyExtraBenefit() { return false; }
    @Override public double getPointAccumulationRate() { return 0.01; }
}

public class VipCustomer extends Customer {
    @Override public int calculateDiscountRate() { return 20; }
    @Override public String getNotificationGreeting() { return "VIP 고객님"; }
    @Override public boolean canApplyExtraBenefit() { return true; }
    @Override public double getPointAccumulationRate() { return 0.05; }
}

public class PartnerCustomer extends Customer {
    @Override public int calculateDiscountRate() { return 30; }
    @Override public String getNotificationGreeting() { return "파트너님"; }
    @Override public boolean canApplyExtraBenefit() { return true; }
    @Override public double getPointAccumulationRate() { return 0.10; }
}

// 3. Service — if 사라짐
public class FareService {
    
    public int calculateDiscount(Customer customer, int amount) {
        return customer.calculateDiscount(amount);  // ← 객체에 위임
    }
    
    public String getNotificationGreeting(Customer customer) {
        return customer.getNotificationGreeting();
    }
    
    public boolean canApplyExtraBenefit(Customer customer) {
        return customer.canApplyExtraBenefit();
    }
    
    public double getPointAccumulationRate(Customer customer) {
        return customer.getPointAccumulationRate();
    }
}

새 등급 (PLATINUM) 추가 — 새 클래스 1개만 ✅

public class PlatinumCustomer extends Customer {
    @Override public int calculateDiscountRate() { return 25; }
    @Override public String getNotificationGreeting() { return "플래티넘 고객님"; }
    @Override public boolean canApplyExtraBenefit() { return true; }
    @Override public double getPointAccumulationRate() { return 0.08; }
}

// 끝!
// FareService 코드는 한 줄도 안 바뀜 ✅
// 다른 등급 클래스도 안 바뀜 ✅

// 사용 시
Customer platinum = new PlatinumCustomer();
fareService.calculateDiscount(platinum, 10000);
// → PlatinumCustomer.calculateDiscount() 자동 호출

4가지 문제가 어떻게 해결됐나?

문제해결
변경 영향 폭발새 클래스 1개만 추가
회귀 위험기존 코드 안 건드림
테스트 폭증새 클래스만 테스트
협업 충돌다른 파일에서 작업

이게 OCP의 진짜 가치.


✅ 4. 해결책 — OCP를 적용하는 방법

OCP의 3가지 핵심 도구

OCP를 가능하게 하는 자바 문법:

1. 인터페이스 (가장 권장) ⭐

public interface DiscountPolicy {
    int calculateDiscount(int amount);
}

public class VipDiscountPolicy implements DiscountPolicy { ... }
public class PartnerDiscountPolicy implements DiscountPolicy { ... }

→ 가장 유연. 현대 자바의 주류.

2. 추상 클래스

public abstract class Customer {
    public abstract int calculateDiscountRate();
}

public class VipCustomer extends Customer { ... }
public class PartnerCustomer extends Customer { ... }

→ 공통 코드 재사용 시. 단, 단일 상속 제약.

3. 합성 + 인터페이스 (가장 유연) ⭐⭐

public class Customer {
    private final DiscountPolicy discountPolicy;  // 가짐 (has-a)
    
    public Customer(DiscountPolicy policy) {
        this.discountPolicy = policy;
    }
    
    public int calculateDiscount(int amount) {
        return discountPolicy.calculateDiscount(amount);
    }
}

// 등급 변경도 자유 (런타임)
customer.changeDiscountPolicy(new VipDiscountPolicy());

상속보다 합성.


OCP 적용 단계 ⭐

단계 1: "변할 부분"과 "안 변할 부분" 분리

[변하는 부분] = 등급별 정책 (할인율, 인사말)
        ↓
   추상화 (인터페이스/추상 클래스)
        ↓
[안 변하는 부분] = 그 추상화를 사용하는 흐름

단계 2: 추상화 정의

public interface DiscountPolicy {
    int calculate(int amount);
}

단계 3: 구현체 생성

public class VipDiscountPolicy implements DiscountPolicy {
    public int calculate(int amount) { return amount * 20 / 100; }
}

단계 4: 흐름은 추상화에 의존

public class FareService {
    public int calculate(int amount, DiscountPolicy policy) {
        return amount - policy.calculate(amount);
    }
}

단계 5: 새 정책 추가 시 — 새 클래스만

public class StudentDiscountPolicy implements DiscountPolicy {
    public int calculate(int amount) { return amount * 15 / 100; }
}
// FareService 안 건드림 ✅

Strategy 패턴 ⭐

OCP의 가장 직접적인 구현 = Strategy 패턴 (5주차에서 본격).

// Strategy 인터페이스
public interface DiscountStrategy {
    int calculate(int amount);
}

// 다양한 전략
public class NoDiscount implements DiscountStrategy { ... }
public class VipDiscount implements DiscountStrategy { ... }
public class SeasonalDiscount implements DiscountStrategy { ... }
public class CouponDiscount implements DiscountStrategy { ... }

// Context — 전략을 사용
public class FareCalculator {
    private DiscountStrategy strategy;
    
    public void setStrategy(DiscountStrategy strategy) {
        this.strategy = strategy;
    }
    
    public int calculate(int amount) {
        return amount - strategy.calculate(amount);
    }
}

새 할인 정책 = 새 Strategy 클래스만.


Spring DI와 OCP ⭐

Spring 자체가 OCP의 거대한 응용:

@Service
public class FareService {
    private final List<DiscountPolicy> policies;
    
    public FareService(List<DiscountPolicy> policies) {
        this.policies = policies;
    }
    
    public int calculateDiscount(Customer customer, int amount) {
        return policies.stream()
            .filter(p -> p.supports(customer))
            .findFirst()
            .map(p -> p.calculate(amount))
            .orElse(0);
    }
}

// 새 정책 추가 — Spring이 자동 주입
@Component
public class HolidayDiscountPolicy implements DiscountPolicy {
    @Override
    public boolean supports(Customer customer) { ... }
    
    @Override
    public int calculate(int amount) { ... }
}
// FareService 안 건드림 ✅

→ Spring의 List<인터페이스> 자동 주입 이 OCP 의 전형적 패턴.


"유연성" vs "복잡성" 의 균형 ⭐

OCP는 추상화 비용 이 따름:

  • 인터페이스 추가
  • 클래스 분리
  • 의존성 주입

모든 곳에 OCP 적용 = 과도한 설계:

// ❌ 단순한 시스템에 과도한 추상화
public interface NameProvider {
    String getName();
}

public class CustomerNameProvider implements NameProvider {
    private String name;
    public String getName() { return name; }
}
// → "이름 가져오기" 가 정말 변하는가? NO → 추상화 불필요

원칙 ⭐ :

"실제로 변화가 자주 일어나는 영역에만 OCP 적용"

판단 기준:

  • 이미 2-3번 변경된 영역 → OCP 적용 가치 ↑
  • 변경 가능성 명확 → OCP 미리 적용
  • 변경이 거의 없음 → OCP 불필요

→ "Rule of Three" — 세 번째 변경에서 추상화 검토.


🏗️ 5. 내부 동작 원리

OCP가 가능한 이유 — 다형성의 응용

OCP는 새 문법이 아닌 다형성의 응용 패턴.

// 1. 인터페이스 (확장 지점)
public interface DiscountPolicy {
    int calculate(int amount);
}

// 2. 호출자는 인터페이스에 의존
public class FareService {
    public int calculate(int amount, DiscountPolicy policy) {
        return amount - policy.calculate(amount);  // ← 다형성 호출
    }
}

// 3. 새 구현체 추가
public class NewPolicy implements DiscountPolicy {
    public int calculate(int amount) { return amount * 50 / 100; }
}

JVM 내부 흐름:

1. fareService.calculate(amount, new NewPolicy()) 호출
        ↓
2. JVM: "policy 변수의 컴파일 타임 타입은? DiscountPolicy"
        ↓
3. JVM: "policy가 가리키는 실제 객체는? NewPolicy 인스턴스"
        ↓
4. JVM: "NewPolicy의 VMT에서 calculate 찾기"
        ↓
5. NewPolicy.calculate() 실행

VMT를 통한 동적 바인딩. Unit 2.4와 동일.


컴파일 시점의 의존성 분석

OCP 위반 코드:

public class FareService {
    public int calculate(int amount, String level) {
        if (level.equals("VIP")) return amount * 20 / 100;
        // ...
    }
}

의존성:

  • FareService → "VIP", "PARTNER", "NORMAL" 같은 구체적 값
  • 새 등급 추가 → FareService 코드 수정 → 컴파일 + 재배포 필요

OCP 적용 코드:

public class FareService {
    public int calculate(int amount, DiscountPolicy policy) {
        return amount - policy.calculate(amount);
    }
}

의존성:

  • FareServiceDiscountPolicy (추상화)
  • 새 구현체 추가 → FareService 컴파일 안 함
  • 재컴파일/재배포 비용 ↓

OCP는 컴파일 단위에서도 효과.


의존성 그래프 비교

OCP 위반:

[FareService]
  ↓ 강한 결합 (구체 타입 참조)
  ↓
"VIP" "PARTNER" "NORMAL" 문자열 비교
새 등급 → FareService 변경

OCP 적용:

[FareService]
  ↓ 약한 결합 (인터페이스 참조)
  ↓
[DiscountPolicy 인터페이스]
       ↑
    구현체 추가
[VipPolicy] [PartnerPolicy] [NewPolicy]

인터페이스가 변경의 방화벽.


클래스 로딩과 OCP

자바 클래스 로딩:

  • JVM은 필요할 때 클래스 로딩
  • 새 구현체 추가 → 그 클래스만 로딩
  • 기존 클래스는 영향 X

Spring의 동적 빈 등록:

@Component
public class NewPolicy implements DiscountPolicy { ... }

→ Spring이 시작 시 자동으로:
1. @Component 클래스 스캔
2. 새 빈 등록
3. List<DiscountPolicy> 에 자동 추가

재시작만으로 새 정책 활성화. 기존 코드 수정 X.


💻 6. 실전 코드 예시

예시 1: 결제 수단의 OCP

// 인터페이스
public interface PaymentMethod {
    void process(int amount);
    String getName();
    boolean supports(String type);
}

// 다양한 구현
@Component
public class CreditCardPayment implements PaymentMethod {
    @Override
    public void process(int amount) {
        System.out.println("신용카드로 " + amount + "원 결제");
    }
    
    @Override
    public String getName() { return "신용카드"; }
    
    @Override
    public boolean supports(String type) {
        return "CREDIT_CARD".equals(type);
    }
}

@Component
public class KakaoPayPayment implements PaymentMethod {
    @Override
    public void process(int amount) {
        System.out.println("카카오페이로 " + amount + "원 결제");
    }
    
    @Override
    public String getName() { return "카카오페이"; }
    
    @Override
    public boolean supports(String type) {
        return "KAKAO_PAY".equals(type);
    }
}

@Component
public class BankTransferPayment implements PaymentMethod {
    @Override
    public void process(int amount) {
        System.out.println("계좌이체로 " + amount + "원 결제");
    }
    
    @Override
    public String getName() { return "계좌이체"; }
    
    @Override
    public boolean supports(String type) {
        return "BANK_TRANSFER".equals(type);
    }
}

// Service — 추상화에 의존
@Service
@RequiredArgsConstructor
public class PaymentService {
    private final List<PaymentMethod> methods;  // Spring이 자동 주입
    
    public void pay(String type, int amount) {
        PaymentMethod method = methods.stream()
            .filter(m -> m.supports(type))
            .findFirst()
            .orElseThrow(() -> new IllegalArgumentException("지원 안 하는 결제 수단"));
        
        method.process(amount);
    }
}

새 결제 수단 (PayPal) 추가:

@Component
public class PayPalPayment implements PaymentMethod {
    @Override
    public void process(int amount) {
        System.out.println("PayPal로 " + amount + "원 결제");
    }
    
    @Override
    public String getName() { return "PayPal"; }
    
    @Override
    public boolean supports(String type) {
        return "PAYPAL".equals(type);
    }
}

PaymentService 한 줄도 안 바뀜. Spring이 자동으로 methods 리스트에 추가.


예시 2: 운임 종류 처리

// 추상 클래스
public abstract class Fare {
    protected Long id;
    protected int amount;
    
    public abstract int calculateTotal();
    public abstract String getDescription();
}

public class StandardFare extends Fare {
    @Override public int calculateTotal() { return amount; }
    @Override public String getDescription() { return "일반 운임"; }
}

public class UrgentFare extends Fare {
    private int urgentFee;
    @Override public int calculateTotal() { return amount + urgentFee; }
    @Override public String getDescription() { return "긴급 운임"; }
}

public class InternationalFare extends Fare {
    private double exchangeRate;
    @Override public int calculateTotal() { return (int)(amount * exchangeRate); }
    @Override public String getDescription() { return "국제 운임"; }
}

// Service — 다형성으로 통일된 처리
@Service
public class FareReportService {
    public void printReport(List<Fare> fares) {
        for (Fare fare : fares) {
            System.out.println(fare.getDescription() + ": " + fare.calculateTotal());
        }
    }
}

새 운임 종류 추가:

public class DomesticFare extends Fare {
    @Override public int calculateTotal() { return amount * 95 / 100; }
    @Override public String getDescription() { return "국내 운임 (5% 할인)"; }
}

FareReportService 안 건드림 ✅.


예시 3: 알림 채널의 OCP

public interface NotificationChannel {
    void send(String message, String recipient);
    boolean supports(NotificationType type);
}

@Component
public class EmailChannel implements NotificationChannel { ... }

@Component
public class SmsChannel implements NotificationChannel { ... }

@Component
public class SlackChannel implements NotificationChannel { ... }

@Component
public class KakaoTalkChannel implements NotificationChannel { ... }

@Service
@RequiredArgsConstructor
public class NotificationService {
    private final List<NotificationChannel> channels;
    
    public void send(NotificationType type, String message, String recipient) {
        channels.stream()
            .filter(c -> c.supports(type))
            .forEach(c -> c.send(message, recipient));
    }
}

새 채널 (Telegram) 추가 → 새 클래스 1개만.


예시 4: ILIC 운임 검증 규칙의 OCP

public interface FareValidationRule {
    void validate(Fare fare);
    int getOrder();  // 검증 순서
}

@Component
@Order(1)
public class FareAmountValidationRule implements FareValidationRule {
    @Override
    public void validate(Fare fare) {
        if (fare.getAmount() < 0) throw new IllegalArgumentException("음수 불가");
        if (fare.getAmount() > 100_000_000) throw new IllegalArgumentException("최대 초과");
    }
}

@Component
@Order(2)
public class FareCustomerValidationRule implements FareValidationRule {
    @Override
    public void validate(Fare fare) {
        if (fare.getCustomerId() == null) throw new IllegalArgumentException("고객 필수");
    }
}

@Component
@Order(3)
public class FareCurrencyValidationRule implements FareValidationRule {
    @Override
    public void validate(Fare fare) {
        if (fare.getCurrency() == null) throw new IllegalArgumentException("통화 필수");
    }
}

@Service
@RequiredArgsConstructor
public class FareValidator {
    private final List<FareValidationRule> rules;  // Spring이 순서대로 주입
    
    public void validate(Fare fare) {
        rules.forEach(rule -> rule.validate(fare));
    }
}

새 검증 규칙 (출발지-도착지 검증) 추가:

@Component
@Order(4)
public class FareRouteValidationRule implements FareValidationRule {
    @Override
    public void validate(Fare fare) {
        if (fare.getOrigin() == null) throw new IllegalArgumentException("출발지 필수");
    }
}

→ 기존 검증 규칙도, FareValidator도 안 건드림 ✅.


예시 5: Java 표준 라이브러리의 OCP

자바 자체가 OCP의 결정체:

// List 인터페이스 — 확장 지점
public interface List<E> {
    void add(E element);
    E get(int index);
    int size();
    // ...
}

// 다양한 구현
public class ArrayList<E> implements List<E> { ... }
public class LinkedList<E> implements List<E> { ... }
public class Vector<E> implements List<E> { ... }
public class CopyOnWriteArrayList<E> implements List<E> { ... }

// 새 구현 추가 가능 — 기존 코드 수정 X
public class MyCustomList<E> implements List<E> { ... }

자바 컬렉션 자체가 OCP의 거대한 응용.


예시 6: Spring AOP가 OCP의 또 다른 응용 ⭐

// 기존 비즈니스 코드
@Service
public class FareService {
    public void registerFare(FareRequest request) {
        // 핵심 비즈니스 로직만
    }
}

// 새 횡단 관심사 추가 — 기존 코드 안 건드림
@Aspect
@Component
public class LoggingAspect {
    @Around("execution(* com.ilic.service.*.*(..))")
    public Object log(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("Before: " + pjp.getSignature());
        Object result = pjp.proceed();
        System.out.println("After");
        return result;
    }
}

로깅 추가에 비즈니스 코드 한 줄도 안 바뀜. 8-9주차에서 본격.


⚠️ 7. 주의사항 & 흔한 실수

실수 1: 모든 곳에 OCP 적용 — 과도한 추상화

// ❌ 단순한 시스템에 과도한 추상화
public interface NameFormatter {
    String format(String name);
}

public class SimpleNameFormatter implements NameFormatter {
    public String format(String name) {
        return name.trim();
    }
}

// 단순 trim에 인터페이스? 과함 ❌

원칙: 변화 가능성이 명확한 영역에만.


실수 2: 추측에 의한 OCP 적용

// ❌ "혹시 모르니까" 추상화
public interface UserNameProvider { ... }
public interface UserAgeProvider { ... }
public interface UserEmailProvider { ... }
// 실제로는 변하지 않는데 미리 인터페이스 ❌

해결: YAGNI (You Aren't Gonna Need It) — 실제 필요할 때.


실수 3: 인터페이스만 추가, 호출자는 구체 타입에 의존

public interface DiscountPolicy {
    int calculate(int amount);
}

public class VipDiscountPolicy implements DiscountPolicy { ... }

// ❌ 호출자가 구체 타입에 의존
public class FareService {
    private VipDiscountPolicy policy;  // ← 인터페이스 아님!
    
    public int calculate(int amount) {
        return policy.calculate(amount);
    }
}
// → OCP 의미 없음, 새 정책 추가 시 FareService 수정 필요

해결: 호출자도 인터페이스에 의존:

public class FareService {
    private DiscountPolicy policy;  // ✅ 인터페이스
}

실수 4: instanceof로 OCP 깨기

public class FareService {
    public int calculate(int amount, DiscountPolicy policy) {
        if (policy instanceof VipDiscountPolicy) {
            // VIP 특별 처리 ❌
        } else if (policy instanceof PartnerDiscountPolicy) {
            // 파트너 특별 처리 ❌
        }
        return policy.calculate(amount);
    }
}

다형성을 외면. 새 정책 추가 시 또 instanceof 추가.

해결: 인터페이스에 메서드 추가하거나 별도 메서드로 분리.


실수 5: 추상화 누수 (Leaky Abstraction)

public interface DiscountPolicy {
    int calculate(int amount);
    String getDatabaseTableName();  // ⚠️ 구현 세부사항 노출
    Connection getConnection();      // ⚠️
}

→ 인터페이스가 구현 세부사항 까지 노출.

해결: 인터페이스는 what 만, how 는 구현체 안에.


실수 6: 인터페이스가 너무 큼

// ❌ 거대 인터페이스
public interface CustomerService {
    Customer create(...);
    Customer update(...);
    void delete(...);
    List<Customer> findAll();
    Customer findById(...);
    void sendEmail(...);
    void sendSms(...);
    void generateReport(...);
    void exportToExcel(...);
    // 50개 메서드 ❌
}

→ 새 구현체 만들 때 모든 메서드 구현 강제 = 부담.

해결: ISP (인터페이스 분리 원칙) — 다음 다음 Unit (3.4).


실수 7: 너무 일찍 OCP 적용 — 비용만 증가

// ❌ 첫 버전부터 너무 많은 추상화
public interface CustomerCreator {
    Customer create(...);
}

public interface CustomerValidator {
    void validate(...);
}

public interface CustomerSaver {
    void save(...);
}
// → 단순한 등록 기능에 5개 인터페이스 ❌

Rule of Three ⭐ :

"한 번은 괜찮다, 두 번은 그래야 할 수도, 세 번이면 추상화하라"

처음에는 단순히 만들고, 변화 패턴이 보이면 그때 OCP.


🔗 8. 연관 개념 맵

Phase 3 (SOLID) 내 흐름

[SRP] — 책임 분리 ✓
   ↓
[OCP] ★ ← 지금 여기 — 다형성으로 확장
   ↓
[LSP] — 안전한 다형성
   ↓
[ISP] — 인터페이스 분리
   ↓
[DIP] — 추상화에 의존

OCP가 SOLID의 중심. 다른 원칙들이 OCP를 강화.


Phase 2와의 연결

Phase 2 학습OCP 적용
Unit 2.3 (상속)추상 클래스로 확장 지점
Unit 2.4 (다형성) ★OCP의 직접적 토대
Unit 2.5 (instanceof)남발하면 OCP 위반
Unit 2.6 (Anonymous)즉석 구현체로 확장

다형성 없는 OCP는 불가능.


미래 주차와의 연결

3주차 (제네릭/람다):

  • 함수형 인터페이스 = OCP의 함수형 표현
  • Stream API = OCP 기반 설계

5주차 (Spring DI):

  • @Component + 인터페이스 = 자동 OCP
  • List<인터페이스> 자동 주입

5주차 (디자인 패턴):

  • Strategy = OCP의 직접 구현
  • Template Method = OCP + 흐름 제어
  • Decorator = OCP + 합성

8-9주차 (AOP):

  • 횡단 관심사 = OCP의 또 다른 응용

11-12주차 (JPA):

  • @Inheritance = OCP 기반 Entity

17주차 (MSA):

  • 서비스 경계 = 거대한 OCP

OCP는 자바 생태계의 토대.


현실적 균형 — OCP의 비용

[OCP 적용]
  ↑ 비용
  - 인터페이스 추가
  - 클래스 분리
  - 의존성 주입
  - 학습 곡선 ↑
  
  ↓ 효과
  - 변경 비용 ↓
  - 테스트 용이
  - 협업 가능
  - 장기 유지보수 ↑

판단 기준:

  • 단기 프로젝트 → OCP 최소 적용
  • 장기 프로젝트 → OCP 적극 적용
  • 변화 자주 발생 영역 → 무조건 OCP

면접 단골 질문 매핑

질문이 Unit에서의 답
"OCP가 뭔가요?"확장에 열려있고, 수정에 닫혀있음
"OCP가 가능한 이유?"다형성 + 추상화 (인터페이스/추상 클래스)
"OCP를 어떻게 적용?"변할 부분 추상화 → 호출자는 추상화에 의존
"Strategy 패턴과 OCP?"Strategy가 OCP의 직접 구현
"Spring DI와 OCP?"Spring 자체가 OCP의 거대한 응용
"OCP의 어려움?"과도한 추상화 위험, Rule of Three로 균형

📝 9. 핵심 요약 — 3줄 정리

1️⃣ OCP는 "확장에는 열려있고, 수정에는 닫혀있다" 의 원칙이다.

새 기능 추가 시 기존 코드를 수정하지 않고 새 코드만 추가 함으로써 변경의 영향을 최소화. 다형성 + 추상화 (인터페이스/추상 클래스) 가 OCP의 핵심 도구. ILIC의 if 지옥은 OCP 위반의 대표 사례.

2️⃣ OCP는 다형성(Unit 2.4) 의 직접적 결과다.

부모 타입으로 자식 객체를 다루는 다형성이 없으면 OCP 불가. Strategy 패턴, Spring DI, JPA, JDBC 모두 OCP의 응용. 자바 생태계가 거대하게 성장한 이유 중 하나가 OCP — 새 구현체 추가에 기존 코드 수정 X.

3️⃣ OCP는 강력하지만 비용이 따른다 — 균형이 핵심.

모든 곳에 OCP 적용은 과도한 추상화. 변화가 실제로 일어나는 영역에만 적용해야 가치. Rule of Three (세 번째 변경에서 추상화 검토) 와 YAGNI (필요할 때 추상화) 의 균형이 시니어의 판단력. 인터페이스가 너무 크면 ISP 위반, 자식이 부모를 못 대체하면 LSP 위반 — 다른 SOLID 원칙들이 OCP를 보완.


🎓 학습 자기 점검

기본 이해

  • OCP의 정의를 한 문장으로 설명할 수 있다
  • OCP가 다형성에 의존하는 이유를 안다
  • OCP의 3가지 도구 (인터페이스, 추상 클래스, 합성) 를 안다
  • Strategy 패턴이 OCP의 구현임을 안다

실전 적용

  • ILIC 코드의 OCP 위반 (if 지옥) 을 식별할 수 있다
  • 다형성으로 OCP를 구현할 수 있다
  • Spring DI를 활용한 OCP 패턴을 작성할 수 있다
  • 과도한 추상화를 피하는 균형감각이 있다

면접 대비 (3-5분 답변)

  • "OCP가 뭔가요?" 답변 가능
  • "OCP를 어떻게 적용하셨나요? ILIC 사례" 답변 가능
  • "OCP의 어려움?" 답변 가능 (과도한 추상화 위험)
  • "다형성과 OCP의 관계?" 답변 가능

자기 점검 — ILIC 적용

박승제님의 ILIC 코드를 점검:

OCP 위반 신호 ⚠️:

  • if (type.equals("VIP")) 같은 분기가 여러 메서드에 반복
  • 새 종류 추가 시 N개 파일 수정해야 함
  • instanceof 분기가 3개 이상
  • enum별 switch 문이 여러 곳에 흩어짐
  • 새 기능 추가 = 기존 클래스 수정

3개 이상 해당 = OCP 적용 가치 큼.


다음 Unit으로

  • LSP (리스코프 치환 원칙) 을 학습할 준비 완료
  • "자식이 부모를 안전하게 대체할 수 있어야" 가 궁금하다
  • OCP의 위험을 LSP가 어떻게 보완하는지 만날 준비 완료
profile
Software Developer

0개의 댓글