이끌든지 따르든지 비키든지

Software Development/Design Pattern

[Design Pattern] LSP(리스코프 치환 원칙)

SeongHo5 2023. 12. 17. 20:43

LSP(Liskov Substitution Principle, 리스코프 치환 원칙)는 SOLID 원칙 중 하나로,

객체 지향 프로그래밍의 설계 원칙에 대한 중요한 개념이다.

 

이 원칙은 1987년 바바라 리스코프에 의해 처음 제안되어 리스코프 치환 원칙이라 부른다.

 


 

리스코프 치환 원칙 (Liskov Substitution Principle)


리스코프 치환 원칙은

 


"프로그램에서 부모 클래스의 인스턴스를 자식 클래스의 인스턴스로 대체(치환) 해도 프로그램의 정확성에 영향을 미치지 않아야 한다."

 

는 내용을 정의하는 원칙이다.

 

이 원칙은 자식 클래스(상속을 받은 클래스)가 부모의 클래스의 동작을 변경하지 않고, 확장해야 함을 말하며, 상속과 다형성을 적절하게 사용하는 방법에 대한 지침을 제공한다 볼 수 있다.

 


 

LSP의 중요성

 

리스코프 치환 원칙을 통해 얻을 수 있는 이점은 크게 3가지이다.


유지보수성: LSP를 준수하면, 기존 코드를 변경하지 않고도 새로운 기능을 추가하거나 기존 기능을 개선할 수 있다. 이는 유지보수를 용이하게 한다.

재사용성: 부모 클래스의 동작을 그대로 이어받으면, 코드 재사용성이 증가한다. 자식 클래스는 부모 클래스의 모든 기능을 사용할 수 있다.

확장성: LSP를 따르면, 시스템을 확장하기가 더 쉬워진다. 새로운 클래스를 추가하거나 기존 클래스를 확장할 때, 기존 시스템에 대한 깊은 이해 없이도 안전하게 변경할 수 있다.

 


 

예시로 살펴보기

 

LSP를 위반한 코드

class Shape {
    public double computeArea() {
        return 0;
    }
}

 

 

Shape라는 상위 클래스와, 이를 상속받는 Rectangle, Square 클래스가 있는 상황이다.

 

class Rectangle extends Shape {
    private double width;
    private double height;

    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    public void setWidth(double width) {
        this.width = width;
    }

    public void setHeight(double height) {
        this.height = height;
    }

    @Override
    public double computeArea() {
        return width * height;
    }
}

 

class Square extends Rectangle {
    public Square(double size) {
        super(size, size);
    }

    @Override
    public void setWidth(double width) {
        super.setWidth(width);
        super.setHeight(width);
    }

    @Override
    public void setHeight(double height) {
        super.setWidth(height);
        super.setHeight(height);
    }
}

 

 

이 경우, Square 클래스는 Rectangle 클래스의 특수한 형태이다. 그러나 정사각형(Square)은 너비와 높이가 항상 같아야 하므로, setWidth나 setHeight를 호출할 때 둘 다 변경되어야 한다.

 

public class Main {
    public static void main(String[] args) {
        Rectangle rect = new Square(5);
        rect.setWidth(10); // Width만 변경하길 기대하지만, Square는 높이도 변경된다.
        System.out.println("Expected Area: 50, Actual Area: " + rect.computeArea());
        // Expected : 10 * 5 = 50, but was 10 * 10 = 100
    }
}

 

이런 상황에, Rectangle 객체를 기대하는 메서드에 Square 객체를 전달하면, 너비와 높이를 개별적으로 설정하는 로직이 제대로 작동하지 않을 수도 있다.

 

이 경우 Square 클래스는 Rectangle의 서브 클래스로 적합하지 않다. Rectangle을 상속받는 Square 클래스는 Rectangle의 동작을 예상대로 확장하지 못하며, 이로 인해 LSP를 위반하기 때문이다.

 

 

 

LSP를 준수하는 코드

class NumberProcessor {
    public void process(Number number) {
        System.out.println("Processing number: " + number);
    }
}

 

 

Number 는 Java에서 모든 숫자 Wrapper 클래스의 부모 클래스이고, 자식 클래스로 Integer, Double, Float 등 이 있다.

 

public class Example {
    public static void main(String[] args) {
        NumberProcessor processor = new NumberProcessor();

        // Integer 처리
        Integer myInt = 5;
        processor.process(myInt);

        // Double 처리
        Double myDouble = 5.99;
        processor.process(myDouble);

        // Float 처리
        Float myFloat = 7.5f;
        processor.process(myFloat);
    }
}

 

NumberProcessor 클래스는 Number 타입의 객체를 매개변수로 받는 process 메소드를 가지고 있다.

이 메소드는 Number의 모든 서브 클래스(Integer, Double, Float 등) 인스턴스에 대해 동일하게 작동한다.