-
SOLID - Liskov substitution 리스코프 치환원칙 파헤치기Java/디자인패턴 2024. 10. 22. 15:27반응형
Liskov substitution principle
**Liskov Substitution Principle (LSP)**는 SOLID 원칙 중 하나로, 자식 클래스는 언제나 부모 클래스를 대체할 수 있어야 한다는 것을 의미합니다. 즉, 자식 클래스가 부모 클래스를 대체하더라도 프로그램의 동작이 일관되어야 한다는 것입니다. 자식 클래스가 부모 클래스에서 제공하는 메서드를 오버라이드할 때, 해당 메서드의 계약을 위반해서는 안 됩니다.
LSP에 위배된 코드 1번
// Parent class Bird public class Bird { public void fly() { System.out.println("Bird is flying"); } } // Child class Penguin that violates LSP public class Penguin extends Bird { @Override public void fly() { // Penguins cannot fly throw new UnsupportedOperationException("Penguins cannot fly"); } } public class Main { public static void main(String[] args) { Bird bird = new Bird(); bird.fly(); // Bird is flying Bird penguin = new Penguin(); penguin.fly(); // Throws UnsupportedOperationException } }
문제점
위 코드에서 Penguin 클래스는 Bird 클래스의 자식 클래스이지만, fly() 메서드를 오버라이드하여 예외를 던지고 있습니다. 이는 LSP를 위반하는 상황입니다. Penguin 클래스는 Bird를 대체할 수 없으며, Bird의 일반적인 행동 규칙(즉, 새는 날 수 있다)을 따르지 않기 때문에 문제를 일으킵니다. 이로 인해 Main 메서드에서 Bird penguin = new Penguin();를 호출하면 UnsupportedOperationException이 발생하여 프로그램이 예기치 않게 종료됩니다.
개선 방법
LSP를 준수하기 위해서는, 자식 클래스가 부모 클래스의 기능을 완전히 대체할 수 있도록 설계를 변경해야 합니다. 새는 모두 날 수 있다는 가정을 하지 말고, 새가 반드시 날 수 있는 것은 아니므로, Bird 클래스를 더 추상적인 수준으로 설계해야 합니다.
이를 해결하기 위한 한 가지 방법은 새의 비행 능력을 별도의 인터페이스로 분리하는 것입니다. Penguin처럼 날 수 없는 새는 이 인터페이스를 구현하지 않도록 할 수 있습니다.
개선된 코드
// 날 수 있는 새를 정의하는 인터페이스 interface Flyable { void fly(); } // Bird 클래스를 추상화하여 모든 새의 공통 행동을 정의 abstract class Bird { public abstract void eat(); // 모든 새는 먹는다 } // Penguin 클래스는 더 이상 날지 않고, Bird 클래스만 상속 class Penguin extends Bird { @Override public void eat() { System.out.println("Penguin is eating"); } // Penguins cannot fly, so no implementation of fly method } // 날 수 있는 새를 표현하는 구체적 클래스 class Sparrow extends Bird implements Flyable { @Override public void fly() { System.out.println("Sparrow is flying"); } @Override public void eat() { System.out.println("Sparrow is eating"); } } public class Main { public static void main(String[] args) { Bird sparrow = new Sparrow(); sparrow.eat(); ((Flyable) sparrow).fly(); // Only Flyable birds can fly Bird penguin = new Penguin(); penguin.eat(); // penguin.fly(); // Not possible because Penguin doesn't implement Flyable } }
개선된 코드 설명
- Bird 클래스: Bird 클래스는 더 추상적인 새의 행동을 정의하며, 모든 새가 가진 공통적인 행동(예: eat())만을 포함합니다.
- Flyable 인터페이스: fly() 메서드는 Flyable 인터페이스로 분리하여, 날 수 있는 새만 이 인터페이스를 구현하도록 했습니다. 따라서 펭귄과 같은 날지 못하는 새는 이 인터페이스를 구현할 필요가 없습니다.
- Penguin 클래스: Penguin은 더 이상 fly() 메서드를 오버라이드하지 않으며, LSP를 위반하지 않습니다. Penguin은 Bird 클래스의 하위 클래스이지만, 부모 클래스의 규약을 위반하지 않습니다.
- Sparrow 클래스: Sparrow는 날 수 있는 새이므로 Flyable 인터페이스를 구현합니다. 이렇게 하면, 비행 능력이 있는 새만 fly() 메서드를 호출할 수 있습니다.
LSP에 위배된 코드 2번
// Base class class Rectangle { protected int width; protected int height; public void setWidth(int width) { this.width = width; } public void setHeight(int height) { this.height = height; } public int getArea() { return width * height; } } // Subclass violating LSP class Square extends Rectangle { @Override public void setWidth(int width) { this.width = width; this.height = width; } @Override public void setHeight(int height) { this.width = height; this.height = height; } } // Client code class AreaCalculator { public void calculateArea(Rectangle rectangle) { rectangle.setWidth(5); rectangle.setHeight(4); System.out.println("Area: " + rectangle.getArea()); // Expected output for Rectangle: Area: 20 // Actual output for Square: Area: 16 (LSP violation) } }
문제점 분석:
- Rectangle과 Square의 불일치: Rectangle의 넓이 계산은 width와 height가 독립적으로 설정될 수 있다는 전제를 따릅니다. 하지만, Square에서는 width와 height가 항상 동일해야 하므로 setWidth() 또는 setHeight() 메서드를 오버라이드할 때, 이 관계가 무너지게 됩니다.
- 예상 결과와 다른 출력: AreaCalculator에서 setWidth(5)와 setHeight(4)를 호출하면 직사각형의 넓이는 5 * 4 = 20이지만, 정사각형에서는 setHeight(4)가 호출되면서 width도 4로 변경되므로 넓이는 16이 됩니다.
개선된 코드
// 도형의 공통 인터페이스 interface Shape { int getArea(); } // 직사각형 클래스 class Rectangle implements Shape { protected int width; protected int height; public Rectangle(int width, int height) { this.width = width; this.height = height; } public void setWidth(int width) { this.width = width; } public void setHeight(int height) { this.height = height; } @Override public int getArea() { return width * height; } } // 정사각형 클래스 class Square implements Shape { private int side; public Square(int side) { this.side = side; } public void setSide(int side) { this.side = side; } @Override public int getArea() { return side * side; } } // AreaCalculator는 이제 Shape 인터페이스에 의존 class AreaCalculator { public void calculateArea(Shape shape) { System.out.println("Area: " + shape.getArea()); } } public class Main { public static void main(String[] args) { // Rectangle 예제 Rectangle rectangle = new Rectangle(5, 4); AreaCalculator calculator = new AreaCalculator(); calculator.calculateArea(rectangle); // 출력: Area: 20 // Square 예제 Square square = new Square(4); calculator.calculateArea(square); // 출력: Area: 16 } }
개선된 코드 설명:
- Shape 인터페이스: Rectangle과 Square가 모두 이 인터페이스를 구현합니다. Shape는 모든 도형이 getArea() 메서드를 가져야 한다는 규칙을 명시합니다.
- Rectangle 클래스: Rectangle은 이제 직사각형의 고유한 속성인 width와 height를 유지하며, 두 속성을 독립적으로 설정할 수 있습니다. 이는 직사각형의 행동에 맞습니다.
- Square 클래스: Square는 더 이상 Rectangle을 상속받지 않고, Shape 인터페이스를 구현하여 정사각형의 고유한 속성인 side를 유지합니다. 이로써 Square는 자신만의 규칙에 따라 동작하며, LSP를 위반하지 않습니다.
- AreaCalculator 클래스: 이 클래스는 이제 Shape 인터페이스에 의존하기 때문에, Rectangle이나 Square를 모두 받아서 넓이를 계산할 수 있습니다. 이는 두 클래스가 서로 다른 동작을 할 수 있음을 명확하게 분리합니다.
LSP 준수
이 설계는 Bird 클래스와 Flyable 인터페이스로 역할을 분리하여, Penguin이 더 이상 fly() 메서드를 통해 예외를 던지지 않도록 했습니다. 이제 Penguin과 같은 날 수 없는 새도 Bird 클래스를 대체할 수 있으며, 프로그램이 일관되게 동작합니다.
반응형'Java > 디자인패턴' 카테고리의 다른 글
Structural Patterns - 파사드(Facade) 파헤치기 (0) 2024.10.24 SOLID - Dependency inversion (의존성 역전법칙)파헤치기 (0) 2024.10.22 SOLID - Interface segregation(인터페이스 분리법칙) 파헤치기 (1) 2024.10.22 SOLID - Open-Closed 개방폐쇄원칙 파헤치기 (0) 2024.10.22 SOLID - SRP 단일 책임의 원칙 파헤치기 (0) 2024.10.22