새로운 기능은 '추가'하고, 기존 코드는 '보호'하라
SOLID의 두 번째 원칙, OCP(Open-Closed Principle)를 알아봅시다.
소프트웨어 요소(클래스, 모듈, 함수 등)는 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다.
쉽게 말해, 새로운 기능이 추가되어도 기존 코드는 절대 수정하지 않도록 설계하라는 뜻입니다.
마치 잘 만든 스마트폰과 같습니다.
스마트폰 제조사는 새로운 앱(기능)이 나올 때마다 스마트폰의 운영체제(OS)를 바꾸지 않습니다.
앱 개발자들이 정해진 규칙(API)에 맞춰 앱을 '추가'만 하면 되도록 시스템을 '개방'해두었죠.
하지만 누구도 운영체제 핵심 코드를 마음대로 '변경'할 수는 없도록 '폐쇄'해두었습니다.
OCP는 바로 이런 설계 철학을 코드에 적용하는 것입니다.
2. 스프링 부트 예제: 새로운 결제 수단 추가하기
온라인 쇼핑몰 프로젝트에서 '카카오페이' 결제 기능을 추가해달라는 요청이 들어왔습니다.
나쁜 예시: 새로운 결제 수단이 추가될 때마다 피곤해지는 서비스 (OCP 위반)
PaymentService 클래스 내부에 if-else 문으로 결제 방식을 분기 처리하는 구조입니다.
문제점: '카카오페이'를 추가하기 위해 PaymentService의 pay 메서드에 else if 구문을 추가해야 합니다.
즉, 기존 코드를 직접 수정해야 합니다.
package com.puzzlix.ocp.bad;
// 나쁜 예시: 새로운 결제 수단이 추가될 때마다 피곤해지는 서비스 (OCP 위반)
class PaymentService {
public void pay(String paymentType, long amount) {
if ("CREDIT_CARD".equals(paymentType)) {
System.out.println(amount + "원을 신용카드로 결제합니다.");
} else if ("BANK_TRANSFER".equals(paymentType)) {
System.out.println(amount + "원을 계좌이체로 결제합니다.");
}
// 여기에 'KAKAOPAY'를 위한 else if를 또 추가해야 한다!
else if ("KAKAOPAY".equals(paymentType)) {
System.out.println(amount + "원을 카카오페이로 결제합니다.");
}
// 네이버페이를 추가하려면 또 여기를 수정해야 한다!
else if ("NAVERPAY".equals(paymentType)) {
System.out.println(amount + "원을 네이버페이로 결제합니다.");
}
else {
throw new IllegalArgumentException("지원하지 않는 결제 방식입니다: " + paymentType);
}
}
// 테스트를 위한 메인 메서드
public static void main(String[] args) {
PaymentService paymentService = new PaymentService();
System.out.println("=== OCP 위반하는 결제 시스템 ===");
// 기존 결제 수단들
paymentService.pay("CREDIT_CARD", 10000);
paymentService.pay("BANK_TRANSFER", 25000);
// 새로 추가된 결제 수단들 (PaymentService 클래스를 수정해야 했음)
paymentService.pay("KAKAOPAY", 15000);
paymentService.pay("NAVERPAY", 30000);
System.out.println("\n*** 문제점 ***");
System.out.println("- 새로운 결제 수단을 추가할 때마다 PaymentService 클래스를 수정해야 합니다.");
System.out.println("- if-else 구문이 계속 늘어나면서 코드가 복잡해집니다.");
System.out.println("- 기존 코드 수정으로 인한 버그 발생 위험이 높습니다.");
System.out.println("- OCP 위반: 확장을 위해 기존 코드를 변경해야 합니다.");
// 지원하지 않는 결제 수단 테스트
try {
paymentService.pay("BITCOIN", 5000);
} catch (IllegalArgumentException e) {
System.out.println("오류: " + e.getMessage());
}
}
}
/**
* === OCP 위반하는 결제 시스템 ===
* 10000원을 신용카드로 결제합니다.
* 25000원을 계좌이체로 결제합니다.
* 15000원을 카카오페이로 결제합니다.
* 30000원을 네이버페이로 결제합니다.
*
* *** 문제점 ***
* - 새로운 결제 수단을 추가할 때마다 PaymentService 클래스를 수정해야 합니다.
* - if-else 구문이 계속 늘어나면서 코드가 복잡해집니다.
* - 기존 코드 수정으로 인한 버그 발생 위험이 높습니다.
* - OCP 위반: 확장을 위해 기존 코드를 변경해야 합니다.
* 오류: 지원하지 않는 결제 방식입니다: BITCOIN
*/
좋은 예시: 어떤 결제 수단이든 환영! 유연한 서비스 (OCP 준수)
PaymentClient라는 역할(인터페이스)을 정의하고, 모든 결제 수단이 이 역할을 수행하도록 약속합니다.
PaymentService는 요청에 맞는 클라이언트를 찾아 결제를 위임합니다.
해결책: '카카오페이' 기능을 추가하고 싶다면,
PaymentClient 인터페이스를 구현한 KakaoPayClient 클래스를 새로 만들기만 하면 됩니다.
PaymentService의 코드는 전혀 수정할 필요가 없습니다.
// 역할(약속) 정의
interface PaymentClient {
boolean supports(String paymentType); // 자신이 처리할 수 있는 결제 타입인지 확인
void pay(long amount);
}
// 신용카드는 PaymentClient 역할을 수행한다.
@Component
class CreditCardClient implements PaymentClient {
@Override
public boolean supports(String paymentType) {
return "CREDIT_CARD".equals(paymentType);
}
@Override
public void pay(long amount) {
System.out.println(amount + "원을 신용카드로 결제합니다.");
}
}
// [새로운 기능 추가!] 카카오페이도 PaymentClient 역할을 수행한다.
@Component
class KakaoPayClient implements PaymentClient {
@Override
public boolean supports(String paymentType) {
return "KAKAOPAY".equals(paymentType);
}
@Override
public void pay(long amount) {
System.out.println(amount + "원을 카카오페이로 결제합니다.");
}
}
// PaymentService는 이제 어떤 결제 클라이언트든 처리할 수 있다.
@Service
class PaymentService {
// 스프링이 @Component로 등록된 모든 PaymentClient 구현체들을 자동으로 주입해준다.
private final List<PaymentClient> paymentClients;
public PaymentService(List<PaymentClient> paymentClients) {
this.paymentClients = paymentClients;
}
// 이 코드는 앞으로 절대 수정할 필요가 없다.
public void pay(String paymentType, long amount) {
PaymentClient client = findClient(paymentType);
client.pay(amount);
}
private PaymentClient findClient(String paymentType) {
return paymentClients.stream()
.filter(client -> client.supports(paymentType))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("지원하지 않는 결제 방식입니다."));
}
}
OCP 핵심 정리
OCP를 지키면 재사용 가능하며, 유지보수가 쉬운 시스템을 만들 수 있습니다.
새로운 요구사항이 생겼을 때, 기존 코드를 건드릴 걱정 없이 새 클래스를 '추가'하는 방식으로 우아하게 대처할 수 있게 됩니다.
'JAVA' 카테고리의 다른 글
| 리플렉션(경직된 설계) (0) | 2025.10.27 |
|---|---|
| 리플렉션 (어노테이션) (0) | 2025.10.27 |
| SRP (Single Responsibility Principle) - 단일 책임 원칙 (0) | 2025.10.15 |
| 객체 지향 설계 - S.O.L.I.D 원칙 (0) | 2025.10.15 |
| [1단계] 프로젝트 초기 설정과 핵심 도메인 설계 (0) | 2025.10.15 |