Home > Backend Development > 📚[Backend Development] 🚀 SOLID 원칙 - 리스코프 치환 원칙(LSP) 트러블슈팅 가이드

📚[Backend Development] 🚀 SOLID 원칙 - 리스코프 치환 원칙(LSP) 트러블슈팅 가이드
Backend Development Spring Boot Trouble Shooting LSP SOLID

🚀 SOLID 원칙 - 리스코프 치환 원칙(LSP) 트러블슈팅 가이드

Spring Boot 개발에서 리스코프 치환 원칙(Liskov Substitution Principle)을 적용하는 과정에서 자주 발생하는 문제와 해결 방법을 정리했습니다.


🔍 문제 1: 자식 클래스에서 예외 발생으로 인한 런타임 오류

📋 에러 상황

결제 시스템에서 포인트 결제를 추가했는데, 환불 기능 호출 시 UnsupportedOperationException이 발생하는 상황입니다.

// 💥 LSP 위반으로 인한 런타임 에러!
@Service
public class PaymentService {
    private final Map<String, PaymentProcessor> processors;

    public void processRefund(String type, double amount) {
        PaymentProcessor processor = processors.get(type);
        processor.refund(amount); // 💥 포인트 결제 시 예외 발생!
    }
}

@Component("point")
public class PointPaymentProcessor implements PaymentProcessor {
    @Override
    public void refund(double amount) {
        // 😱 부모의 행위 규약 위반!
        throw new UnsupportedOperationException("포인트 결제는 환불이 불가능합니다.");
    }
}

🎯 원인 분석

리스코프 치환 원칙(LSP)을 위반한 설계로, 자식 클래스가 부모의 계약을 지키지 못해 다형성이 안전하지 않습니다.

  1. 계약 위반: 자식 클래스가 부모 인터페이스의 행위 규약을 지키지 않음
  2. 타입 안전성 부족: 클라이언트 코드에서 구체 타입을 확인해야 함
  3. 런타임 에러: 컴파일 시점에 발견되지 않는 예외 발생
  4. 다형성 파괴: 부모 타입으로 안전하게 치환할 수 없음

🔧 해결 방법

1단계: 역할에 따른 인터페이스 분리

// 🎯 결제 기본 기능만 정의
public interface Payable {
    void processPayment(double amount);
}

// 🔄 환불까지 포함하는 확장 인터페이스
public interface Refundable extends Payable {
    void refund(double amount);
}

핵심 포인트

  • Payable: 모든 결제 수단의 최소 공통 기능
  • Refundable: 환불까지 지원하는 결제 수단의 확장 기능

2단계: 역할에 맞는 구현체 작성

// ✅ 카드 결제: 결제와 환불 모두 가능
@Component("card")
public class CardPaymentProcessor implements Refundable {
    
    @Override
    public void processPayment(double amount) {
        System.out.println(amount + "원을 카드로 결제합니다.");
        // 카드 결제 API 호출
    }
    
    @Override
    public void refund(double amount) {
        System.out.println(amount + "원을 카드로 환불합니다.");
        // 카드 환불 API 호출 - LSP 준수!
    }
}

// ✅ 포인트 결제: 결제만 가능 (예외 발생 없음)
@Component("point")
public class PointPaymentProcessor implements Payable {
    
    @Override
    public void processPayment(double amount) {
        System.out.println(amount + " 포인트를 사용하여 결제합니다.");
        // 포인트 차감 로직 - LSP 준수!
    }
    
    // 🎯 refund() 메서드가 없어서 예외 발생 자체가 불가능!
}

3단계: LSP 준수하는 안전한 서비스 클래스

// 🚀 타입 안전성이 보장된 결제 서비스
@Service
@RequiredArgsConstructor
public class PaymentService {
    
    private final Map<String, Payable> payableProcessors;
    private final Map<String, Refundable> refundableProcessors;
    
    // ✅ 모든 결제 수단에서 안전하게 동작
    public void processPayment(String type, double amount) {
        Payable processor = payableProcessors.get(type);
        if (processor == null) {
            throw new IllegalArgumentException("지원하지 않는 결제 타입입니다: " + type);
        }
        processor.processPayment(amount); // LSP 보장!
    }
    
    // ✅ 환불 지원 결제 수단에서만 안전하게 동작
    public void processRefund(String type, double amount) {
        Refundable processor = refundableProcessors.get(type);
        if (processor == null) {
            throw new UnsupportedOperationException("이 결제 타입은 환불을 지원하지 않습니다: " + type);
        }
        processor.refund(amount); // LSP 보장!
    }
}

📚 LSP 핵심 개념

원칙 의미 Spring Boot 구현 방법
행위 규약 준수 자식은 부모의 계약을 지켜야 함 인터페이스 메서드에서 예외 발생 금지
안전한 치환 부모 타입으로 완전 대체 가능 적절한 추상화와 인터페이스 분리
사전조건 강화 금지 자식이 더 엄격한 조건 요구 불가 파라미터 검증 완화만 허용
사후조건 약화 금지 자식이 더 약한 결과 반환 불가 반환값의 계약 준수

🔍 문제 2: 자식 클래스의 사전조건 강화로 인한 호환성 문제

📋 에러 상황

도형 계산 시스템에서 정사각형이 직사각형을 상속받았는데, 너비와 높이 설정에서 제약이 생기는 상황입니다.

// 😱 LSP 위반: 자식 클래스가 더 강한 사전조건을 요구
@Component
public class Rectangle {
    protected double width;
    protected double height;
    
    public void setWidth(double width) {
        this.width = width;
    }
    
    public void setHeight(double height) {
        this.height = height;
    }
    
    public double getArea() {
        return width * height;
    }
}

@Component
public class Square extends Rectangle {
    
    @Override
    public void setWidth(double width) {
        // 💥 사전조건 강화! 부모보다 더 엄격한 제약
        if (width != height && height != 0) {
            throw new IllegalArgumentException("정사각형은 너비와 높이가 같아야 합니다!");
        }
        this.width = width;
        this.height = width; // 강제로 같게 만듦
    }
    
    @Override
    public void setHeight(double height) {
        // 💥 사전조건 강화! 부모보다 더 엄격한 제약
        if (height != width && width != 0) {
            throw new IllegalArgumentException("정사각형은 너비와 높이가 같아야 합니다!");
        }
        this.height = height;
        this.width = height; // 강제로 같게 만듦
    }
}

🎯 원인 분석

자식 클래스가 부모 클래스보다 강화된 사전조건을 요구하여 LSP를 위반합니다.

  1. 사전조건 강화: 정사각형이 직사각형보다 더 엄격한 조건 요구
  2. 예상치 못한 동작: 클라이언트 코드가 예상과 다르게 동작
  3. 다형성 파괴: Rectangle 타입으로 Square 사용 시 예외 발생
  4. 설계 오류: “is-a” 관계를 잘못 적용

🔧 해결 방법

1단계: 불변 객체 패턴으로 재설계

// 🎯 도형의 공통 기능만 추상화
public abstract class Shape {
    public abstract double getArea();
    public abstract double getPerimeter();
    public abstract String getShapeType();
}

// ✅ 불변 직사각형 - LSP 준수
@Component
public class Rectangle extends Shape {
    private final double width;
    private final double height;
    
    public Rectangle(double width, double height) {
        if (width <= 0 || height <= 0) {
            throw new IllegalArgumentException("너비와 높이는 양수여야 합니다.");
        }
        this.width = width;
        this.height = height;
    }
    
    @Override
    public double getArea() {
        return width * height;
    }
    
    @Override
    public double getPerimeter() {
        return 2 * (width + height);
    }
    
    @Override
    public String getShapeType() {
        return "Rectangle";
    }
    
    // Getter만 제공 (불변성 보장)
    public double getWidth() { return width; }
    public double getHeight() { return height; }
}

// ✅ 불변 정사각형 - LSP 준수
@Component
public class Square extends Shape {
    private final double side;
    
    public Square(double side) {
        if (side <= 0) {
            throw new IllegalArgumentException("한 변의 길이는 양수여야 합니다.");
        }
        this.side = side;
    }
    
    @Override
    public double getArea() {
        return side * side; // 부모와 동일한 계약 준수
    }
    
    @Override
    public double getPerimeter() {
        return 4 * side; // 부모와 동일한 계약 준수
    }
    
    @Override
    public String getShapeType() {
        return "Square";
    }
    
    public double getSide() { return side; }
}

2단계: 팩토리 패턴으로 안전한 생성

// 🏭 도형 생성을 위한 팩토리
@Service
public class ShapeFactory {
    
    public Rectangle createRectangle(double width, double height) {
        return new Rectangle(width, height);
    }
    
    public Square createSquare(double side) {
        return new Square(side);
    }
    
    // 🎯 타입에 따른 안전한 도형 생성
    public Shape createShape(String type, double... dimensions) {
        return switch (type.toUpperCase()) {
            case "RECTANGLE" -> {
                if (dimensions.length != 2) {
                    throw new IllegalArgumentException("직사각형은 너비와 높이가 필요합니다.");
                }
                yield createRectangle(dimensions[0], dimensions[1]);
            }
            case "SQUARE" -> {
                if (dimensions.length != 1) {
                    throw new IllegalArgumentException("정사각형은 한 변의 길이가 필요합니다.");
                }
                yield createSquare(dimensions[0]);
            }
            default -> throw new IllegalArgumentException("지원하지 않는 도형 타입: " + type);
        };
    }
}

3단계: LSP 준수하는 도형 계산 서비스

// 🧮 모든 도형에서 안전하게 동작하는 계산 서비스
@Service
@RequiredArgsConstructor
public class ShapeCalculationService {
    
    private final ShapeFactory shapeFactory;
    
    // ✅ 모든 Shape 하위 클래스에서 안전하게 동작
    public double calculateTotalArea(List<Shape> shapes) {
        return shapes.stream()
                .mapToDouble(Shape::getArea) // LSP 보장!
                .sum();
    }
    
    // ✅ 도형 타입에 관계없이 안전하게 동작
    public String generateReport(List<Shape> shapes) {
        StringBuilder report = new StringBuilder();
        
        for (Shape shape : shapes) {
            report.append(String.format("%s - 넓이: %.2f, 둘레: %.2f%n",
                    shape.getShapeType(), // LSP 보장!
                    shape.getArea(),      // LSP 보장!
                    shape.getPerimeter()  // LSP 보장!
            ));
        }
        
        return report.toString();
    }
    
    // 🎯 다양한 도형 조합 계산
    public ShapeStatistics calculateStatistics(List<Shape> shapes) {
        return ShapeStatistics.builder()
                .totalShapes(shapes.size())
                .totalArea(calculateTotalArea(shapes))
                .averageArea(shapes.stream().mapToDouble(Shape::getArea).average().orElse(0))
                .largestArea(shapes.stream().mapToDouble(Shape::getArea).max().orElse(0))
                .build();
    }
}

🔍 문제 3: 상속 계층에서의 복잡한 LSP 위반

📋 에러 상황

새와 펭귄의 상속 관계에서 fly() 메서드 때문에 LSP가 위반되는 복잡한 상황입니다.

// 😱 LSP 위반: 모든 새가 날 수 있다는 잘못된 추상화
public abstract class Bird {
    public abstract void fly();
    public abstract void eat();
}

public class Penguin extends Bird {
    @Override
    public void fly() {
        // 💥 LSP 위반! 펭귄은 날 수 없음
        throw new UnsupportedOperationException("펭귄은 날 수 없습니다!");
    }
    
    @Override
    public void eat() {
        System.out.println("물고기를 먹습니다.");
    }
}

🎯 원인 분석

잘못된 추상화로 인해 일부 자식 클래스가 부모의 모든 기능을 지원할 수 없어 LSP를 위반합니다.

  1. 과도한 추상화: 모든 새가 날 수 있다는 잘못된 가정
  2. 기능별 분리 부족: 비행 능력과 새 자체를 분리하지 못함
  3. 인터페이스 분리 원칙 위반: 사용하지 않는 기능을 강제로 구현
  4. 다중 책임: 하나의 클래스가 여러 능력을 모두 처리

🔧 해결 방법

1단계: 능력별 인터페이스 분리

// 🐦 모든 새의 기본 능력
public interface Bird {
    void eat();
    void makeSound();
    String getSpecies();
}

// ✈️ 비행 능력 (별도 인터페이스)
public interface Flyable {
    void fly();
    double getFlightSpeed();
    double getMaxAltitude();
}

// 🏊 수영 능력 (별도 인터페이스)
public interface Swimmable {
    void swim();
    double getSwimmingSpeed();
    double getMaxDepth();
}

// 🚶 보행 능력 (별도 인터페이스)
public interface Walkable {
    void walk();
    double getWalkingSpeed();
}

2단계: 능력에 따른 구체적 구현

// 🦅 독수리: 날고 걸을 수 있음
@Component
public class Eagle implements Bird, Flyable, Walkable {
    
    @Override
    public void eat() {
        System.out.println("작은 동물을 잡아먹습니다.");
    }
    
    @Override
    public void makeSound() {
        System.out.println("끼야악!");
    }
    
    @Override
    public String getSpecies() {
        return "독수리";
    }
    
    @Override
    public void fly() {
        System.out.println("높이 날아오릅니다.");
    }
    
    @Override
    public double getFlightSpeed() {
        return 80.0; // km/h
    }
    
    @Override
    public double getMaxAltitude() {
        return 3000.0; // m
    }
    
    @Override
    public void walk() {
        System.out.println("땅에서 걸어다닙니다.");
    }
    
    @Override
    public double getWalkingSpeed() {
        return 5.0; // km/h
    }
}

// 🐧 펭귄: 수영하고 걸을 수 있음 (날지 못함!)
@Component
public class Penguin implements Bird, Swimmable, Walkable {
    
    @Override
    public void eat() {
        System.out.println("물고기를 먹습니다.");
    }
    
    @Override
    public void makeSound() {
        System.out.println("꽥꽥!");
    }
    
    @Override
    public String getSpecies() {
        return "펭귄";
    }
    
    @Override
    public void swim() {
        System.out.println("물속에서 빠르게 수영합니다.");
    }
    
    @Override
    public double getSwimmingSpeed() {
        return 25.0; // km/h
    }
    
    @Override
    public double getMaxDepth() {
        return 500.0; // m
    }
    
    @Override
    public void walk() {
        System.out.println("뒤뚱뒤뚱 걸어다닙니다.");
    }
    
    @Override
    public double getWalkingSpeed() {
        return 2.0; // km/h
    }
}

// 🐔 닭: 걸을 수만 있음 (날지도 헤엄치지도 못함!)
@Component
public class Chicken implements Bird, Walkable {
    
    @Override
    public void eat() {
        System.out.println("곡식을 쪼아먹습니다.");
    }
    
    @Override
    public void makeSound() {
        System.out.println("꼬끼오!");
    }
    
    @Override
    public String getSpecies() {
        return "닭";
    }
    
    @Override
    public void walk() {
        System.out.println("땅에서 걸어다닙니다.");
    }
    
    @Override
    public double getWalkingSpeed() {
        return 15.0; // km/h
    }
}

3단계: LSP 준수하는 새 관리 서비스

// 🎯 능력별로 안전하게 새들을 관리하는 서비스
@Service
@RequiredArgsConstructor
public class BirdManagementService {
    
    private final List<Bird> allBirds;
    
    // ✅ 모든 새에서 안전하게 동작
    public void feedAllBirds() {
        allBirds.forEach(bird -> {
            System.out.println(bird.getSpecies() + "에게 먹이를 줍니다.");
            bird.eat(); // LSP 보장!
        });
    }
    
    // ✅ 비행 가능한 새들만 안전하게 처리
    public void organizeFlightShow() {
        List<Flyable> flyingBirds = allBirds.stream()
                .filter(bird -> bird instanceof Flyable)
                .map(bird -> (Flyable) bird)
                .collect(Collectors.toList());
        
        flyingBirds.forEach(flyable -> {
            System.out.println("비행 쇼 시작!");
            flyable.fly(); // LSP 보장!
        });
    }
    
    // ✅ 수영 가능한 새들만 안전하게 처리
    public void organizeSwimmingCompetition() {
        List<Swimmable> swimmingBirds = allBirds.stream()
                .filter(bird -> bird instanceof Swimmable)
                .map(bird -> (Swimmable) bird)
                .collect(Collectors.toList());
        
        swimmingBirds.forEach(swimmable -> {
            System.out.println("수영 대회 시작!");
            swimmable.swim(); // LSP 보장!
        });
    }
    
    // 🏃 모든 새의 보행 시합 (대부분의 새가 걸을 수 있음)
    public void organizeWalkingRace() {
        allBirds.stream()
                .filter(bird -> bird instanceof Walkable)
                .map(bird -> (Walkable) bird)
                .forEach(walkable -> {
                    System.out.println("걷기 시합 참가!");
                    walkable.walk(); // LSP 보장!
                });
    }
    
    // 📊 새들의 능력 통계
    public BirdCapabilityStatistics getCapabilityStatistics() {
        long flyingCount = allBirds.stream().mapToLong(bird -> bird instanceof Flyable ? 1 : 0).sum();
        long swimmingCount = allBirds.stream().mapToLong(bird -> bird instanceof Swimmable ? 1 : 0).sum();
        long walkingCount = allBirds.stream().mapToLong(bird -> bird instanceof Walkable ? 1 : 0).sum();
        
        return BirdCapabilityStatistics.builder()
                .totalBirds(allBirds.size())
                .flyingBirds(flyingCount)
                .swimmingBirds(swimmingCount)
                .walkingBirds(walkingCount)
                .build();
    }
}

🎨 LSP 실무 활용 패턴

비즈니스 도메인 잘못된 추상화 올바른 LSP 설계
파일 처리 FileProcessor.compress() (모든 파일이 압축 가능?) Compressible 인터페이스 분리
사용자 권한 User.adminAction() (모든 사용자가 관리자?) AdminUser, RegularUser 별도 타입
결제 수단 Payment.refund() (모든 결제가 환불 가능?) Refundable 인터페이스 분리
운송 수단 Vehicle.fly() (모든 운송 수단이 비행?) Flyable, Drivable 능력별 분리

📊 LSP 체크리스트

✅ 설계 검증

  • 자식 클래스에서 부모 메서드 호출 시 예외가 발생하지 않는가?
  • 자식 클래스가 부모보다 강한 사전조건을 요구하지 않는가?
  • 자식 클래스가 부모보다 약한 사후조건을 제공하지 않는가?
  • instanceof 체크 없이 다형성을 사용할 수 있는가?

✅ 구현 품질

  • 인터페이스가 클라이언트의 필요에 따라 적절히 분리되었는가?
  • 불변 객체 패턴을 활용하여 상태 변경 문제를 방지했는가?
  • 팩토리 패턴으로 객체 생성을 안전하게 관리하는가?
  • 각 클래스가 자신의 책임만 가지고 있는가?

✅ 확장성 고려

  • 새로운 하위 타입 추가 시 기존 코드가 깨지지 않는가?
  • 능력별로 인터페이스가 분리되어 유연한 조합이 가능한가?
  • 컴파일 타임에 타입 안전성이 보장되는가?

🎯 실전 활용 예시

E-commerce 상품 관리 시스템에서의 LSP 활용

// 🎯 상품의 기본 기능
public interface Product {
    String getName();
    BigDecimal getPrice();
    String getDescription();
    boolean isAvailable();
}

// 📦 실물 상품 능력
public interface PhysicalProduct extends Product {
    double getWeight();
    Dimensions getDimensions();
    ShippingInfo calculateShipping(String destination);
}

// 💾 디지털 상품 능력
public interface DigitalProduct extends Product {
    String getDownloadUrl();
    long getFileSize();
    List<String> getSupportedFormats();
    void sendDownloadLink(String email);
}

// 📚 구독 상품 능력
public interface SubscriptionProduct extends Product {
    Period getSubscriptionPeriod();
    BigDecimal getRecurringPrice();
    LocalDate getNextBillingDate();
    void renewSubscription();
}

// ✅ 책 (실물 상품)
@Component
public class Book implements PhysicalProduct {
    
    private final String title;
    private final BigDecimal price;
    private final String author;
    private final double weight;
    private final Dimensions dimensions;
    
    // PhysicalProduct의 모든 메서드를 완전히 구현 (LSP 준수)
    @Override
    public String getName() { return title; }
    
    @Override
    public BigDecimal getPrice() { return price; }
    
    @Override
    public String getDescription() { 
        return String.format("저자: %s", author); 
    }
    
    @Override
    public boolean isAvailable() { 
        return true; // 재고 확인 로직
    }
    
    @Override
    public double getWeight() { return weight; }
    
    @Override
    public Dimensions getDimensions() { return dimensions; }
    
    @Override
    public ShippingInfo calculateShipping(String destination) {
        // 실제 배송비 계산 - LSP 준수!
        return ShippingCalculator.calculate(weight, dimensions, destination);
    }
}

// ✅ 전자책 (디지털 상품)
@Component
public class EBook implements DigitalProduct {
    
    private final String title;
    private final BigDecimal price;
    private final String downloadUrl;
    private final long fileSize;
    
    // DigitalProduct의 모든 메서드를 완전히 구현 (LSP 준수)
    @Override
    public String getName() { return title; }
    
    @Override
    public BigDecimal getPrice() { return price; }
    
    @Override
    public String getDescription() { 
        return String.format("파일 크기: %d MB", fileSize / 1024 / 1024); 
    }
    
    @Override
    public boolean isAvailable() { 
        return true; // 디지털 상품은 항상 사용 가능
    }
    
    @Override
    public String getDownloadUrl() { return downloadUrl; }
    
    @Override
    public long getFileSize() { return fileSize; }
    
    @Override
    public List<String> getSupportedFormats() { 
        return List.of("PDF", "EPUB", "MOBI"); 
    }
    
    @Override
    public void sendDownloadLink(String email) {
        // 실제 이메일 발송 - LSP 준수!
        EmailService.sendDownloadLink(email, downloadUrl);
    }
}

LSP 준수하는 상품 관리 서비스

// 🛒 타입별로 안전하게 상품을 처리하는 서비스
@Service
@RequiredArgsConstructor
public class ProductManagementService {
    
    private final List<Product> allProducts;
    private final ShippingService shippingService;
    private final EmailService emailService;
    
    // ✅ 모든 상품에서 안전하게 동작
    public List<ProductSummary> getAllProductSummaries() {
        return allProducts.stream()
                .map(product -> ProductSummary.builder()
                        .name(product.getName())        // LSP 보장!
                        .price(product.getPrice())      // LSP 보장!
                        .description(product.getDescription()) // LSP 보장!
                        .available(product.isAvailable())      // LSP 보장!
                        .build())
                .collect(Collectors.toList());
    }
    
    // ✅ 실물 상품만 안전하게 처리
    public List<ShippingEstimate> calculateShippingEstimates(String destination) {
        return allProducts.stream()
                .filter(product -> product instanceof PhysicalProduct)
                .map(product -> (PhysicalProduct) product)
                .map(physical -> ShippingEstimate.builder()
                        .productName(physical.getName())
                        .shippingInfo(physical.calculateShipping(destination)) // LSP 보장!
                        .build())
                .collect(Collectors.toList());
    }
    
    // ✅ 디지털 상품만 안전하게 처리
    public void sendDigitalDeliveries(String customerEmail) {
        allProducts.stream()
                .filter(product -> product instanceof DigitalProduct)
                .map(product -> (DigitalProduct) product)
                .forEach(digital -> {
                    System.out.printf("디지털 상품 전달: %s%n", digital.getName());
                    digital.sendDownloadLink(customerEmail); // LSP 보장!
                });
    }
    
    // 🔄 구독 상품 갱신
    public void renewSubscriptions() {
        allProducts.stream()
                .filter(product -> product instanceof SubscriptionProduct)
                .map(product -> (SubscriptionProduct) product)
                .forEach(subscription -> {
                    System.out.printf("구독 갱신: %s%n", subscription.getName());
                    subscription.renewSubscription(); // LSP 보장!
                });
    }
}

새로운 상품 타입 추가 - 멤버십

// 🎯 새로운 멤버십 상품 - 기존 코드 수정 없이 추가!
@Component
public class MembershipProduct implements SubscriptionProduct {
    
    private final String membershipName;
    private final BigDecimal monthlyFee;
    private final List<String> benefits;
    
    // SubscriptionProduct의 모든 메서드를 완전히 구현 (LSP 준수)
    @Override
    public String getName() { return membershipName; }
    
    @Override
    public BigDecimal getPrice() { return monthlyFee; }
    
    @Override
    public String getDescription() {
        return String.format("혜택: %s", String.join(", ", benefits));
    }
    
    @Override
    public boolean isAvailable() { return true; }
    
    @Override
    public Period getSubscriptionPeriod() { 
        return Period.ofMonths(1); 
    }
    
    @Override
    public BigDecimal getRecurringPrice() { 
        return monthlyFee; 
    }
    
    @Override
    public LocalDate getNextBillingDate() {
        return LocalDate.now().plusMonths(1);
    }
    
    @Override
    public void renewSubscription() {
        // 멤버십 갱신 로직 - LSP 준수!
        System.out.println("멤버십이 자동으로 갱신되었습니다.");
        // 결제 처리 및 혜택 연장
    }
}

결과: 기존 ProductManagementService나 다른 상품 클래스들은 단 한 줄도 수정하지 않고 새로운 멤버십 상품 기능이 완벽하게 추가됩니다! 🎉


📈 성능 고려사항

✅ 최적화 팁

  • instanceof 체크를 최소화하여 런타임 성능 향상
  • 인터페이스별 Map 활용으로 O(1) 조회 구현
  • 캐싱으로 반복적인 타입 체크 방지
// 🚀 성능 최적화된 LSP 구현
@Service
@RequiredArgsConstructor
public class OptimizedProductService {
    
    // 타입별로 미리 분류하여 성능 최적화
    private final Map<Class<?>, List<Product>> productsByType;
    private final Map<String, PhysicalProduct> physicalProductsById;
    private final Map<String, DigitalProduct> digitalProductsById;
    
    @PostConstruct
    public void initializeProductMaps() {
        // 애플리케이션 시작 시 한 번만 분류
        List<Product> allProducts = productRepository.findAll();
        
        this.productsByType = allProducts.stream()
                .collect(Collectors.groupingBy(Object::getClass));
        
        this.physicalProductsById = allProducts.stream()
                .filter(product -> product instanceof PhysicalProduct)
                .collect(Collectors.toMap(
                        Product::getId,
                        product -> (PhysicalProduct) product
                ));
                
        this.digitalProductsById = allProducts.stream()
                .filter(product -> product instanceof DigitalProduct)
                .collect(Collectors.toMap(
                        Product::getId,
                        product -> (DigitalProduct) product
                ));
    }
    
    // 🎯 O(1) 조회로 빠른 배송비 계산
    public ShippingInfo calculateShippingFast(String productId, String destination) {
        PhysicalProduct product = physicalProductsById.get(productId);
        if (product == null) {
            throw new IllegalArgumentException("실물 상품이 아닙니다: " + productId);
        }
        return product.calculateShipping(destination); // LSP 보장!
    }
}

🎉 마무리

이제 SOLID LSP 원칙을 활용한 안전하고 확장 가능한 Spring Boot 애플리케이션을 만들 수 있습니다!

🚀 다음 단계 권장사항

  1. 다른 SOLID 원칙과의 조합: SRP + LSP + ISP를 함께 적용
  2. 디자인 패턴 활용: Strategy, State, Template Method 패턴과 LSP
  3. 테스트 전략: LSP 준수를 검증하는 단위 테스트 작성

📞 추가 학습 리소스

  • Clean Architecture (Robert C. Martin): 올바른 추상화와 경계 설정
  • Effective Java 3rd Edition: 상속보다는 컴포지션 활용법
  • Spring Boot Testing: LSP 준수를 검증하는 테스트 방법

💡 핵심 기억할 점

LSP는 “자식이 부모의 약속을 반드시 지켜야 한다”는 원칙입니다. Spring Boot의 인터페이스 분리와 DI를 활용하면 타입 안전하고 예측 가능한 다형성을 구현할 수 있어 견고한 객체 지향 시스템을 구축할 수 있습니다!