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 위에 서 있다.
집에 콘센트가 있습니다. 220V 표준 규격이라 모든 가전제품이 호환:
새 가전제품이 나오면? 콘센트만 표준대로 만들면 끝:
만약 콘센트에 표준이 없다면?
핵심:
→ 이게 OCP의 정신.
스마트폰의 운영체제(iOS, Android)는 새 앱이 추가될 때마다 OS를 수정하지 않습니다:
만약 새 앱 추가할 때마다 OS를 업데이트해야 한다면?
OS의 비밀:
→ OCP가 곧 플랫폼 사고방식.
"기존 코드를 수정하지 않고 새 기능을 추가할 수 있어야 한다."
OCP의 두 가지 측면:
비유 정리:
| 비유 요소 | OCP 적용 |
|---|---|
| 콘센트 표준 | 인터페이스/추상 클래스 |
| 새 가전제품 | 새 구현체 (확장) |
| 콘센트 수정 X | 기존 코드 변경 X |
| 표준 미준수 | 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의 토대]
↓ "새 객체를 추가해도 기존 호출자는 그대로"
↓
[OCP 달성 ✅]
핵심 통찰:
"OCP는 다형성을 응용한 설계 원칙이다"
다형성 없는 OCP는 불가능. 두 개념은 하나의 사고.
자바 생태계가 거대하게 성장한 이유 중 하나가 OCP:
Spring:
JPA:
JDBC:
거의 모든 자바 프레임워크 = OCP의 결과.
"OCP는 '변화를 예상하고 미리 설계하는' 원칙이다."
새 기능이 추가될 때마다 기존 코드를 수정한다면, 시스템은 수정의 무게에 짓눌려 결국 동작 안 함. 확장 지점을 미리 만들어둠 으로써 변화를 자연스럽게 흡수할 수 있다.
그러나 모든 변화를 예측할 수는 없으니, 실제로 변화가 일어나는 영역에만 OCP를 적용하는 균형이 필요.
OCP를 위반했을 때의 구체적 문제를 ILIC 시나리오로 보겠습니다.
새 고객 등급이 추가될 때마다 처리 로직이 늘어나는 시스템.
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;
}
}
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개 메서드 수정 ❌
FareService 가 100개 메서드를 가지고 있다면 → 100개 메서드 수정.
→ 수정 비용 폭발.
기존 동작이 깨질 가능성:
// 의도: VIP 분기 추가
else if (customer.getLevel().equals("PLATINUM")) { // 새 추가
return amount * 25 / 100;
}
// 실수로 기존 VIP 로직을 건드림 ❌
→ 새 기능 추가 시 기존 기능 깨질 위험.
여러 개발자가 동시에 새 등급 추가 시:
// 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();
}
}
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() 자동 호출
| 문제 | 해결 |
|---|---|
| 변경 영향 폭발 | 새 클래스 1개만 추가 |
| 회귀 위험 | 기존 코드 안 건드림 |
| 테스트 폭증 | 새 클래스만 테스트 |
| 협업 충돌 | 다른 파일에서 작업 |
→ 이게 OCP의 진짜 가치.
OCP를 가능하게 하는 자바 문법:
public interface DiscountPolicy {
int calculateDiscount(int amount);
}
public class VipDiscountPolicy implements DiscountPolicy { ... }
public class PartnerDiscountPolicy implements DiscountPolicy { ... }
→ 가장 유연. 현대 자바의 주류.
public abstract class Customer {
public abstract int calculateDiscountRate();
}
public class VipCustomer extends Customer { ... }
public class PartnerCustomer extends Customer { ... }
→ 공통 코드 재사용 시. 단, 단일 상속 제약.
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());
→ 상속보다 합성.
[변하는 부분] = 등급별 정책 (할인율, 인사말)
↓
추상화 (인터페이스/추상 클래스)
↓
[안 변하는 부분] = 그 추상화를 사용하는 흐름
public interface DiscountPolicy {
int calculate(int amount);
}
public class VipDiscountPolicy implements DiscountPolicy {
public int calculate(int amount) { return amount * 20 / 100; }
}
public class FareService {
public int calculate(int amount, DiscountPolicy policy) {
return amount - policy.calculate(amount);
}
}
public class StudentDiscountPolicy implements DiscountPolicy {
public int calculate(int amount) { return amount * 15 / 100; }
}
// FareService 안 건드림 ✅
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 자체가 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 의 전형적 패턴.
OCP는 추상화 비용 이 따름:
모든 곳에 OCP 적용 = 과도한 설계:
// ❌ 단순한 시스템에 과도한 추상화
public interface NameProvider {
String getName();
}
public class CustomerNameProvider implements NameProvider {
private String name;
public String getName() { return name; }
}
// → "이름 가져오기" 가 정말 변하는가? NO → 추상화 불필요
원칙 ⭐ :
"실제로 변화가 자주 일어나는 영역에만 OCP 적용"
판단 기준:
→ "Rule of Three" — 세 번째 변경에서 추상화 검토.
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);
}
}
의존성:
FareService → DiscountPolicy (추상화)FareService 컴파일 안 함→ OCP는 컴파일 단위에서도 효과.
OCP 위반:
[FareService]
↓ 강한 결합 (구체 타입 참조)
↓
"VIP" "PARTNER" "NORMAL" 문자열 비교
새 등급 → FareService 변경
OCP 적용:
[FareService]
↓ 약한 결합 (인터페이스 참조)
↓
[DiscountPolicy 인터페이스]
↑
구현체 추가
[VipPolicy] [PartnerPolicy] [NewPolicy]
→ 인터페이스가 변경의 방화벽.
자바 클래스 로딩:
Spring의 동적 빈 등록:
@Component
public class NewPolicy implements DiscountPolicy { ... }
→ Spring이 시작 시 자동으로:
1. @Component 클래스 스캔
2. 새 빈 등록
3. List<DiscountPolicy> 에 자동 추가
→ 재시작만으로 새 정책 활성화. 기존 코드 수정 X.
// 인터페이스
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 리스트에 추가.
// 추상 클래스
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 안 건드림 ✅.
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개만.
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도 안 건드림 ✅.
자바 자체가 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의 거대한 응용.
// 기존 비즈니스 코드
@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주차에서 본격.
// ❌ 단순한 시스템에 과도한 추상화
public interface NameFormatter {
String format(String name);
}
public class SimpleNameFormatter implements NameFormatter {
public String format(String name) {
return name.trim();
}
}
// 단순 trim에 인터페이스? 과함 ❌
원칙: 변화 가능성이 명확한 영역에만.
// ❌ "혹시 모르니까" 추상화
public interface UserNameProvider { ... }
public interface UserAgeProvider { ... }
public interface UserEmailProvider { ... }
// 실제로는 변하지 않는데 미리 인터페이스 ❌
해결: YAGNI (You Aren't Gonna Need It) — 실제 필요할 때.
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; // ✅ 인터페이스
}
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 추가.
해결: 인터페이스에 메서드 추가하거나 별도 메서드로 분리.
public interface DiscountPolicy {
int calculate(int amount);
String getDatabaseTableName(); // ⚠️ 구현 세부사항 노출
Connection getConnection(); // ⚠️
}
→ 인터페이스가 구현 세부사항 까지 노출.
해결: 인터페이스는 what 만, how 는 구현체 안에.
// ❌ 거대 인터페이스
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).
// ❌ 첫 버전부터 너무 많은 추상화
public interface CustomerCreator {
Customer create(...);
}
public interface CustomerValidator {
void validate(...);
}
public interface CustomerSaver {
void save(...);
}
// → 단순한 등록 기능에 5개 인터페이스 ❌
Rule of Three ⭐ :
"한 번은 괜찮다, 두 번은 그래야 할 수도, 세 번이면 추상화하라"
처음에는 단순히 만들고, 변화 패턴이 보이면 그때 OCP.
[SRP] — 책임 분리 ✓
↓
[OCP] ★ ← 지금 여기 — 다형성으로 확장
↓
[LSP] — 안전한 다형성
↓
[ISP] — 인터페이스 분리
↓
[DIP] — 추상화에 의존
→ OCP가 SOLID의 중심. 다른 원칙들이 OCP를 강화.
| Phase 2 학습 | OCP 적용 |
|---|---|
| Unit 2.3 (상속) | 추상 클래스로 확장 지점 |
| Unit 2.4 (다형성) ★ | OCP의 직접적 토대 |
| Unit 2.5 (instanceof) | 남발하면 OCP 위반 |
| Unit 2.6 (Anonymous) | 즉석 구현체로 확장 |
→ 다형성 없는 OCP는 불가능.
3주차 (제네릭/람다):
5주차 (Spring DI):
@Component + 인터페이스 = 자동 OCPList<인터페이스> 자동 주입5주차 (디자인 패턴):
8-9주차 (AOP):
11-12주차 (JPA):
17주차 (MSA):
→ OCP는 자바 생태계의 토대.
[OCP 적용]
↑ 비용
- 인터페이스 추가
- 클래스 분리
- 의존성 주입
- 학습 곡선 ↑
↓ 효과
- 변경 비용 ↓
- 테스트 용이
- 협업 가능
- 장기 유지보수 ↑
판단 기준:
| 질문 | 이 Unit에서의 답 |
|---|---|
| "OCP가 뭔가요?" | 확장에 열려있고, 수정에 닫혀있음 |
| "OCP가 가능한 이유?" | 다형성 + 추상화 (인터페이스/추상 클래스) |
| "OCP를 어떻게 적용?" | 변할 부분 추상화 → 호출자는 추상화에 의존 |
| "Strategy 패턴과 OCP?" | Strategy가 OCP의 직접 구현 |
| "Spring DI와 OCP?" | Spring 자체가 OCP의 거대한 응용 |
| "OCP의 어려움?" | 과도한 추상화 위험, Rule of Three로 균형 |
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를 보완.
박승제님의 ILIC 코드를 점검:
OCP 위반 신호 ⚠️:
if (type.equals("VIP")) 같은 분기가 여러 메서드에 반복instanceof 분기가 3개 이상3개 이상 해당 = OCP 적용 가치 큼.