ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 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
        }
    }

     

    개선된 코드 설명

    1. Bird 클래스: Bird 클래스는 더 추상적인 새의 행동을 정의하며, 모든 새가 가진 공통적인 행동(예: eat())만을 포함합니다.
    2. Flyable 인터페이스: fly() 메서드는 Flyable 인터페이스로 분리하여, 날 수 있는 새만 이 인터페이스를 구현하도록 했습니다. 따라서 펭귄과 같은 날지 못하는 새는 이 인터페이스를 구현할 필요가 없습니다.
    3. Penguin 클래스: Penguin은 더 이상 fly() 메서드를 오버라이드하지 않으며, LSP를 위반하지 않습니다. Penguin은 Bird 클래스의 하위 클래스이지만, 부모 클래스의 규약을 위반하지 않습니다.
    4. 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
        }
    }

    개선된 코드 설명:

    1. Shape 인터페이스: Rectangle과 Square가 모두 이 인터페이스를 구현합니다. Shape는 모든 도형이 getArea() 메서드를 가져야 한다는 규칙을 명시합니다.
    2. Rectangle 클래스: Rectangle은 이제 직사각형의 고유한 속성인 width와 height를 유지하며, 두 속성을 독립적으로 설정할 수 있습니다. 이는 직사각형의 행동에 맞습니다.
    3. Square 클래스: Square는 더 이상 Rectangle을 상속받지 않고, Shape 인터페이스를 구현하여 정사각형의 고유한 속성인 side를 유지합니다. 이로써 Square는 자신만의 규칙에 따라 동작하며, LSP를 위반하지 않습니다.
    4. AreaCalculator 클래스: 이 클래스는 이제 Shape 인터페이스에 의존하기 때문에, Rectangle이나 Square를 모두 받아서 넓이를 계산할 수 있습니다. 이는 두 클래스가 서로 다른 동작을 할 수 있음을 명확하게 분리합니다.

     

    LSP 준수

     

    이 설계는 Bird 클래스와 Flyable 인터페이스로 역할을 분리하여, Penguin이 더 이상 fly() 메서드를 통해 예외를 던지지 않도록 했습니다. 이제 Penguin과 같은 날 수 없는 새도 Bird 클래스를 대체할 수 있으며, 프로그램이 일관되게 동작합니다.

    반응형

    댓글

Designed by Tistory.