개발 이야기/OOP

[SOLID] 리스코프 치환 원칙(LSP)이란?

제이온 (Jayon) 2021. 3. 11.

안녕하세요? 제이온입니다.

 

저번 시간에는 개방 폐쇄 원칙에 대해서 알아 보았습니다. 오늘은 리스코프 치환 원칙을 설명하겠습니다.

 

 

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

리스코프 치환 원칙은 "상위 타입의 객체를 하위 타입의 객체로 치환해도 상위 타입을 사용하는 프로그램은 정상적으로 동작해야 한다."를 의미합니다.

 

이를 쉽게 말하면, 특정 메소드가 상위 타입을 인자로 사용한다고 할 때, 그 타입의 하위 타입도 문제 없이 정상적으로 작동을 해야 한다는 것입니다.

 

 

public int calculate(final Item item) {
	return item.calculate();
}

 

상위 타입인 Item이 있고, 그 하위 타입이 Apple이라면, 위 인자로 item이 아닌 apple을 넘겨도 코드가 동작해야 합니다.

 

여기서, 리스코프 치환 원칙이 제대로 지켜지지 않으면 다형성에 기반한 개방 폐쇄 원칙 역시 위반하는 것이기 때문에, 리스코프 치환 원칙을 지키는 것이 중요합니다.

 

 

리스코프 치환 원칙을 위반할 때의 문제

리스코프 치환 원칙을 지키지 않는 가장 대표적인 예가 '직사각형-정사각형 문제'입니다. 우리는 직사각형은 정사각형이 아니지만, 정사각형은 직사각형이라는 사실을 알고 있습니다.

 

 

public class Rectangle {
    private int width;
    private int height;

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

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

    public int getWidth() {
        return width;
    }

    public int getHeight() {
        return height;
    }
}

 

 

따라서 위의 직사각형 객체를 상속받아서 아래처럼 정사각형 객체를 정의할 수 있습니다.

 

 

public class Square extends Rectangle {

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

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

 

 

다만, 정사각형은 가로와 세로의 길이가 같으므로 setWidth()나 setHeight()를 호출하면 가로와 세로를 모두 값을 바꿔줘야해서 메소드를 재정의하였습니다.

 

 

이제, 다른 클래스에서 Rectangle 클래스를 이용해 봅시다.

 

 

    public void increaseHeight(final Rectangle rectangle) {
        if (rectangle.getHeight() <= rectangle.getWidth()) {
            rectangle.setHeight(rectangle.getWidth() + 1);
        }
    }

 

 

해당 메소드는 직사각형의 가로와 세로를 비교한 다음에, 세로가 가로보다 짧거나 같다면 가로의 길이에 1을 더한 만큼의 길이를 갖게 만드는 역할을 합니다. 정사각형이 아닌 직사각형에 대해서는 위 메소드가 올바르게 작동합니다. 예를 들어, 가로가 3, 세로가 2라고 한다면, 세로의 길이는 4가 되는 것이죠.

 

하지만, 정사각형의 경우는 다릅니다. 정사각형은 항상 가로와 세로의 길이가 같으므로 위 메소드를 실행하게 되면 가로와 세로의 길이가 모두 1씩 증가하게 됩니다.

 

즉, 우리가 원하는 "메소드 실행 후, 직사각형의 길이는 가로보다 세로가 길어야 한다."는 가정이 깨지게 되는 것입니다. 따라서, instanceof를 통해 타입 비교를 해야 합니다.

 

 

    public void increaseHeight(final Rectangle rectangle) {
        if (rectangle instanceof Square) {
            throw new IllegalStateException();
        }

        if (rectangle.getHeight() <= rectangle.getWidth()) {
            rectangle.setHeight(rectangle.getWidth() + 1);
        }
    }

 

 

이렇게 해당 도형이 정사각형일 경우 익셉션을 발생시키는 식으로 코드를 수정할 수 있습니다. 하지만, 이것은 개방 폐쇄 원칙에 어긋나는 코드입니다. 왜냐하면, increaseHeight()가 확장에는 열려 있지 않기 때문이죠.

 

따라서,Square 클래스는 Rectangle 클래스를 상속받으면 안 됩니다. 아무리 우리가 '정사각형은 직사각형이다.'라고 이야기를 해도 위처럼 문제가 발생하였기 때문이죠.

 

 

리스코프 치환 원칙은 기능의 명세와 확장에 대한 것

리스코프 치환 원칙은 기능의 명세에 대한 내용입니다. 앞서 직사각형-정사각형 문제 예에서 Rectangle 클래스의 setHeight() 메소드는 아래와 같은 기능의 명세를 제공합니다.

 

 

- 높이 값을 파라미터로 전달받은 값으로 변경한다.

- 폭 값은 변경되지 않는다.

 

 

즉, Rectangle 클래스의 setHeight()는 높이만 변경되고 폭은 그대로 유지될 것으로 가정하는데, Square 클래스의 setHeight()는 높이와 폭이 모두 바뀌게 됩니다. 따라서, 상위 타입에서 정한 명세를 하위 타입에서도 그대로 지킬 수 있을 때 상속을 해야 합니다. (혹은, 상속을 하게 된다면 해당 메소드를 정의하지 않아야 합니다.)

 

 

정리

리스코프 치환 원칙이 지켜지지 않으면 개방 폐쇄 원칙을 위반하게 되므로 기능 확장을 위해 더 많은 부분을 수정해야 합니다. 이것은 확장을 어렵게 하는 것이죠. 따라서, 우리는 상속을 잘 정의하여 치환 가능성을 위배되지 않도록 설계해야 합니다.

 

 

출처

개발자가 반드시 정복해야 할 객체 지향과 디자인 패턴 - 최범균

 

 

SOLID 법칙 中 LID

인터페이스 분리 원칙, 리스코프 치환 원칙, 의존성 역전 원칙에 대해 알아보기

medium.com

 

댓글

추천 글