Home > Backend Development > 📚[Backend Development] 🚀 SOLID 원칙 - 개방-폐쇄 원칙(OCP) 트러블슈팅 가이드

📚[Backend Development] 🚀 SOLID 원칙 - 개방-폐쇄 원칙(OCP) 트러블슈팅 가이드
Backend Development Spring Boot Trouble Shooting OCP SOLID

🚀 SOLID 원칙 - 개방-폐쇄 원칙(OCP) 트러블슈팅 가이드

Spring Boot 개발에서 개방-폐쇄 원칙(Open/Closed Principle)을 적용하는 과정에서 자주 발생하는 문제와 해결 방법을 정리했습니다.


🔍 문제 1: 기능 추가 시 기존 코드 수정 불가피

📋 에러 상황

새로운 알림 방식을 추가할 때마다 기존 서비스 클래스를 직접 수정해야 하는 상황이 발생합니다.

@Service
public class NotificationService {
    
    public void sendNotification(String type, String message) {
        if ("KAKAO".equals(type)) {
            System.out.println("카카오톡 발송: " + message);
        } else if ("FACEBOOK".equals(type)) {
            System.out.println("페이스북 발송: " + message);
        } else if ("SLACK".equals(type)) {
            // 😱 새로운 기능 추가 시 기존 코드 수정 필요!
            System.out.println("슬랙 발송: " + message);
        }
        // 계속해서 else if 구문이 늘어남...
    }
}

🎯 원인 분석

개방-폐쇄 원칙(OCP)을 위반한 설계로, 확장성과 유지보수성이 저하됩니다.

  1. 수정에 닫혀있지 않음: 새로운 기능 추가 시 기존 코드 직접 수정 필요
  2. 확장성 부족: if-else 블록이 계속 증가하여 복잡도 상승
  3. 테스트 어려움: 새 기능 테스트 시 전체 서비스를 테스트해야 함
  4. 단일 책임 원칙 위반: 하나의 클래스가 모든 알림 로직을 담당

🔧 해결 방법

1단계: 공통 기능 추상화

// 🎯 알림 발송의 공통 계약 정의
public interface Notifier {
    void send(String message);
    boolean supports(String type);
}

핵심 포인트

  • send(): 실제 알림 발송 로직
  • supports(): 지원하는 알림 타입 확인

2단계: 구체적인 구현 클래스 생성

// ✅ 카카오톡 알림 구현
@Component
public class KakaoNotifier implements Notifier {
    
    @Override
    public void send(String message) {
        System.out.println("카카오톡 발송: " + message);
        // 실제 카카오톡 API 호출 로직
    }
    
    @Override
    public boolean supports(String type) {
        return "KAKAO".equalsIgnoreCase(type);
    }
}

// ✅ 페이스북 알림 구현
@Component
public class FacebookNotifier implements Notifier {
    
    @Override
    public void send(String message) {
        System.out.println("페이스북 발송: " + message);
        // 실제 페이스북 API 호출 로직
    }
    
    @Override
    public boolean supports(String type) {
        return "FACEBOOK".equalsIgnoreCase(type);
    }
}

3단계: OCP 준수하는 서비스 클래스

// 🚀 수정에 닫혀있고 확장에 열려있는 서비스
@Service
@RequiredArgsConstructor
public class NotificationService {
    
    // 🎯 Spring이 Notifier 구현체들을 자동으로 주입
    private final List<Notifier> notifiers;
    
    // ✅ 새로운 알림 방식이 추가되어도 이 코드는 절대 수정되지 않음!
    public void sendNotification(String type, String message) {
        Notifier notifier = findNotifier(type);
        notifier.send(message);
    }
    
    private Notifier findNotifier(String type) {
        return notifiers.stream()
                .filter(n -> n.supports(type))
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException(
                    "지원하지 않는 알림 타입입니다: " + type));
    }
}

📚 OCP 핵심 개념

원칙 의미 Spring Boot 구현 방법
확장에 열려있음 새로운 기능 추가 가능 @Component로 새 구현체 생성
수정에 닫혀있음 기존 코드 변경 없음 인터페이스와 DI 활용
추상화 활용 구체적 구현에서 분리 Interface 또는 Abstract Class

🔍 문제 2: 새로운 기능 확장의 복잡성

📋 에러 상황

슬랙 알림 기능을 추가해야 하는데, 기존 코드 여러 곳을 수정해야 한다고 생각하는 경우입니다.

🎯 원인 분석

OCP를 제대로 구현했다면 새로운 기능 추가가 매우 간단해야 합니다.

🔧 해결 방법

OCP 준수 시 - 새로운 기능 추가는 이렇게 간단합니다!

// 🎉 새로운 슬랙 알림 기능 - 단 한 개의 새 클래스만 추가!
@Component
public class SlackNotifier implements Notifier {
    
    @Value("${slack.webhook.url}")
    private String webhookUrl;
    
    @Override
    public void send(String message) {
        // 🔧 슬랙 웹훅 API 호출 로직
        System.out.println("슬랙 발송: " + message);
        // RestTemplate 또는 WebClient로 실제 API 호출
    }
    
    @Override
    public boolean supports(String type) {
        return "SLACK".equalsIgnoreCase(type);
    }
}

놀라운 점

  • 기존 NotificationService 코드: 0줄 수정 🎯
  • 기존 다른 Notifier 클래스들: 0줄 수정 🎯
  • 새로 추가한 코드: 단 1개 클래스
  • Spring이 자동으로 Bean 등록 및 주입 처리 🚀

🔍 문제 3: 복잡한 비즈니스 로직에서의 OCP 적용

📋 에러 상황

할인 정책, 결제 방식, 배송 방식 등 복잡한 비즈니스 로직에서 OCP를 어떻게 적용할지 막막한 경우입니다.

🎯 원인 분석

단순한 if-else 분기를 넘어서, 실제 비즈니스 도메인에서 OCP를 적용하는 패턴을 이해하지 못한 경우입니다.

🔧 해결 방법

실전 예시: 전자상거래 할인 정책 시스템

// 🎯 할인 정책의 공통 계약
public interface DiscountPolicy {
    BigDecimal calculateDiscount(Order order);
    boolean isApplicable(Order order);
    int getPriority(); // 여러 할인 정책 적용 시 우선순위
}

// 🛍️ 회원 등급 할인
@Component
public class MembershipDiscountPolicy implements DiscountPolicy {
    
    @Override
    public BigDecimal calculateDiscount(Order order) {
        Customer customer = order.getCustomer();
        BigDecimal totalAmount = order.getTotalAmount();
        
        return switch (customer.getMembershipLevel()) {
            case GOLD -> totalAmount.multiply(new BigDecimal("0.15"));
            case SILVER -> totalAmount.multiply(new BigDecimal("0.10"));
            case BRONZE -> totalAmount.multiply(new BigDecimal("0.05"));
            default -> BigDecimal.ZERO;
        };
    }
    
    @Override
    public boolean isApplicable(Order order) {
        return order.getCustomer().getMembershipLevel() != MembershipLevel.NONE;
    }
    
    @Override
    public int getPriority() {
        return 1; // 높은 우선순위
    }
}

// 🎊 쿠폰 할인
@Component
public class CouponDiscountPolicy implements DiscountPolicy {
    
    private final CouponService couponService;
    
    @Override
    public BigDecimal calculateDiscount(Order order) {
        return order.getCoupons().stream()
                .filter(coupon -> couponService.isValid(coupon))
                .map(Coupon::getDiscountAmount)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
    
    @Override
    public boolean isApplicable(Order order) {
        return !order.getCoupons().isEmpty();
    }
    
    @Override
    public int getPriority() {
        return 2;
    }
}

// 📦 수량 할인
@Component
public class BulkDiscountPolicy implements DiscountPolicy {
    
    @Override
    public BigDecimal calculateDiscount(Order order) {
        int totalQuantity = order.getItems().stream()
                .mapToInt(OrderItem::getQuantity)
                .sum();
                
        if (totalQuantity >= 10) {
            return order.getTotalAmount().multiply(new BigDecimal("0.20"));
        } else if (totalQuantity >= 5) {
            return order.getTotalAmount().multiply(new BigDecimal("0.10"));
        }
        
        return BigDecimal.ZERO;
    }
    
    @Override
    public boolean isApplicable(Order order) {
        return order.getItems().stream()
                .mapToInt(OrderItem::getQuantity)
                .sum() >= 5;
    }
    
    @Override
    public int getPriority() {
        return 3;
    }
}

할인 적용 서비스

// 🎯 할인 정책들을 조합하여 최적의 할인 계산
@Service
@RequiredArgsConstructor
public class DiscountService {
    
    private final List<DiscountPolicy> discountPolicies;
    
    // ✅ 모든 적용 가능한 할인 정책 중 최대 할인 금액 계산
    public BigDecimal calculateMaxDiscount(Order order) {
        return discountPolicies.stream()
                .filter(policy -> policy.isApplicable(order))
                .map(policy -> policy.calculateDiscount(order))
                .max(BigDecimal::compareTo)
                .orElse(BigDecimal.ZERO);
    }
    
    // 📊 적용 가능한 모든 할인 정책 정보 반환
    public List<DiscountInfo> getApplicableDiscounts(Order order) {
        return discountPolicies.stream()
                .filter(policy -> policy.isApplicable(order))
                .sorted(Comparator.comparing(DiscountPolicy::getPriority))
                .map(policy -> DiscountInfo.builder()
                        .policyName(policy.getClass().getSimpleName())
                        .discountAmount(policy.calculateDiscount(order))
                        .priority(policy.getPriority())
                        .build())
                .collect(Collectors.toList());
    }
}

🎨 OCP 실무 활용 패턴

비즈니스 도메인 인터페이스 예시 구현체 예시
결제 처리 PaymentProcessor CreditCardProcessor, PayPalProcessor, BankTransferProcessor
배송 방식 ShippingCalculator StandardShipping, ExpressShipping, OvernightShipping
인증 방식 AuthenticationProvider DatabaseAuthProvider, OAuthProvider, LdapAuthProvider
파일 저장 FileStorage LocalFileStorage, S3FileStorage, GoogleCloudStorage

📊 OCP 체크리스트

✅ 설계 검증

  • 새로운 기능 추가 시 기존 코드를 수정하지 않아도 되는가?
  • 인터페이스나 추상 클래스를 통한 추상화가 적절한가?
  • Spring의 DI를 활용하여 구현체들이 자동으로 주입되는가?

✅ 구현 품질

  • 각 구현 클래스가 단일 책임을 가지고 있는가?
  • if-else 분기문이 제거되었는가?
  • 새로운 구현체 추가가 단순한가? (하나의 클래스만 추가)

✅ 확장성 고려

  • 우선순위나 조건부 적용이 필요한 경우 고려되었는가?
  • 여러 구현체를 조합하여 사용할 수 있는가?
  • 성능에 영향을 주지 않는가?

🎯 실전 활용 예시

E-commerce 주문 처리 시스템에서의 OCP 활용

// 🎯 주문 처리 단계별 전략 패턴 + OCP 적용
public interface OrderProcessor {
    void process(Order order);
    boolean canProcess(OrderType orderType);
    int getProcessingOrder();
}

@Component
public class RegularOrderProcessor implements OrderProcessor {
    
    @Override
    public void process(Order order) {
        // 📦 일반 주문 처리 로직
        validateStock(order);
        reserveInventory(order);
        createShippingLabel(order);
        sendConfirmationEmail(order);
    }
    
    @Override
    public boolean canProcess(OrderType orderType) {
        return OrderType.REGULAR.equals(orderType);
    }
    
    @Override
    public int getProcessingOrder() {
        return 1;
    }
}

@Component
public class PreOrderProcessor implements OrderProcessor {
    
    @Override
    public void process(Order order) {
        // 🔮 예약 주문 처리 로직
        validatePreOrderConditions(order);
        scheduleInventoryReservation(order);
        sendPreOrderConfirmation(order);
        createPaymentSchedule(order);
    }
    
    @Override
    public boolean canProcess(OrderType orderType) {
        return OrderType.PRE_ORDER.equals(orderType);
    }
    
    @Override
    public int getProcessingOrder() {
        return 2;
    }
}

// 🎯 주문 처리 조정자
@Service
@RequiredArgsConstructor
public class OrderService {
    
    private final List<OrderProcessor> orderProcessors;
    
    @Transactional
    public void processOrder(Order order) {
        OrderProcessor processor = findProcessor(order.getOrderType());
        processor.process(order);
        
        // 📊 공통 후처리 작업
        order.markAsProcessed();
        publishOrderProcessedEvent(order);
    }
    
    private OrderProcessor findProcessor(OrderType orderType) {
        return orderProcessors.stream()
                .filter(processor -> processor.canProcess(orderType))
                .min(Comparator.comparing(OrderProcessor::getProcessingOrder))
                .orElseThrow(() -> new IllegalArgumentException(
                    "지원하지 않는 주문 타입입니다: " + orderType));
    }
}

새로운 주문 타입 추가 - 긴급 주문

// 🚨 긴급 주문 처리기 - 새 클래스만 추가하면 끝!
@Component
public class UrgentOrderProcessor implements OrderProcessor {
    
    private final PriorityInventoryService priorityInventoryService;
    private final ExpressShippingService expressShippingService;
    
    @Override
    public void process(Order order) {
        // ⚡ 긴급 주문 전용 고속 처리 로직
        priorityInventoryService.immediateReservation(order);
        expressShippingService.scheduleUrgentDelivery(order);
        sendUrgentOrderNotification(order);
        applyUrgentProcessingFee(order);
    }
    
    @Override
    public boolean canProcess(OrderType orderType) {
        return OrderType.URGENT.equals(orderType);
    }
    
    @Override
    public int getProcessingOrder() {
        return 0; // 최우선 처리
    }
}

결과: 기존 OrderService나 다른 Processor 클래스들은 단 한 줄도 수정하지 않고 새로운 긴급 주문 기능이 완벽하게 추가됩니다! 🎉


📈 성능 고려사항

✅ 최적화 팁

  • List<T> 주입보다는 Map<String, T> 활용으로 O(1) 조회
  • @Lazy 어노테이션으로 필요한 시점에만 초기화
  • 캐싱 전략 적용으로 반복 계산 방지
// 🚀 성능 최적화된 버전
@Service
@RequiredArgsConstructor
public class OptimizedNotificationService {
    
    private final Map<String, Notifier> notifierMap;
    
    // 🎯 생성자에서 Map 구성 (O(1) 조회를 위해)
    public OptimizedNotificationService(List<Notifier> notifiers) {
        this.notifierMap = notifiers.stream()
                .collect(Collectors.toMap(
                    notifier -> extractType(notifier),
                    Function.identity()
                ));
    }
    
    public void sendNotification(String type, String message) {
        Notifier notifier = notifierMap.get(type.toUpperCase());
        if (notifier == null) {
            throw new IllegalArgumentException("지원하지 않는 알림 타입: " + type);
        }
        notifier.send(message);
    }
}

🎉 마무리

이제 SOLID OCP 원칙을 활용한 확장 가능하고 유지보수가 쉬운 Spring Boot 애플리케이션을 만들 수 있습니다!

🚀 다음 단계 권장사항

  1. 다른 SOLID 원칙 학습: SRP, LSP, ISP, DIP와 OCP의 조합 활용
  2. 디자인 패턴 적용: Strategy, Factory, Template Method 패턴과 OCP
  3. Spring Boot 고급 기능: @Conditional 어노테이션을 활용한 동적 Bean 등록

📞 추가 학습 리소스

  • Clean Code (Robert C. Martin): SOLID 원칙의 바이블
  • Spring Boot Reference Documentation: 의존성 주입 및 자동 설정
  • Effective Java 3rd Edition: 인터페이스와 추상화 활용법

💡 핵심 기억할 점

OCP는 “변화에 유연하게 대응하는 시스템”을 만드는 핵심 원칙입니다. Spring Boot의 DI 컨테이너와 인터페이스를 활용하면 새로운 기능은 쉽게 추가하고, 기존 코드는 안정적으로 유지할 수 있는 견고한 아키텍처를 구축할 수 있습니다!