🚀 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단계: 역할에 따른 인터페이스 분리
// 🎯 결제 기본 기능만 정의
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를 위반합니다.
- 사전조건 강화: 정사각형이 직사각형보다 더 엄격한 조건 요구
- 예상치 못한 동작: 클라이언트 코드가 예상과 다르게 동작
- 다형성 파괴: Rectangle 타입으로 Square 사용 시 예외 발생
- 설계 오류: “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단계: 능력별 인터페이스 분리
// 🐦 모든 새의 기본 능력
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 애플리케이션을 만들 수 있습니다!
🚀 다음 단계 권장사항
- 다른 SOLID 원칙과의 조합: SRP + LSP + ISP를 함께 적용
- 디자인 패턴 활용: Strategy, State, Template Method 패턴과 LSP
- 테스트 전략: LSP 준수를 검증하는 단위 테스트 작성
📞 추가 학습 리소스
- Clean Architecture (Robert C. Martin): 올바른 추상화와 경계 설정
- Effective Java 3rd Edition: 상속보다는 컴포지션 활용법
- Spring Boot Testing: LSP 준수를 검증하는 테스트 방법
💡 핵심 기억할 점
LSP는 “자식이 부모의 약속을 반드시 지켜야 한다”는 원칙입니다. Spring Boot의 인터페이스 분리와 DI를 활용하면 타입 안전하고 예측 가능한 다형성을 구현할 수 있어 견고한 객체 지향 시스템을 구축할 수 있습니다!