객체지향프로그래밍

SOLID 5원칙 - LSP 리스코프 치환 원칙(Liskov Substitution Principle)

쿠쿠s 2022. 1. 19. 10:47

 

 

프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다. 다형성에서 하위 클래스는 인터페이스 규약을 다 지켜야 한다는 것, 다형성을 지원하기 위한 원칙, 인터페이스를 구현한 구현 체는 믿고 사용하려면, 이 원칙이 필요하다. 단순히 컴파일을 실패하고 성공하는 것을 얘기하는 것이 아니다.

 

즉, 상위타입에서 어떤 동작하는 기능이 있는데 이를 확장한 하위타입에서도 상위타입과 동일하게 동작해야 합니다.

 

위키백과에 실려있는 전형적인 위반의 예로 코드를 만들어 보겠습니다.

너비와 높이의 조회(getter) 및 할당(setter) 메서드를 가진 직사각형 클래스로부터 정사각형 클래스를 파생하는 경우를 들 수 있습니다.

편의상 getter는 생략하겠습니다.

package SOLID;

public class LSPExam {

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

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

        public void setHeight(int height) {
            this.height = height;
        }
        
        public int getArea() {
            return this.height * this.width;
        }
    }

    static class Square extends Rectangle{

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

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

        @Override
        public int getArea() {
            return super.getArea();
        }
    }

    public static void main(String[] args) {
        Rectangle rectangle = new Rectangle();
        rectangle.setWidth(3);
        rectangle.setHeight(4);
        rectangle.getArea();
     
        //정사각형
        Rectangle square = new Square();
        square.setWidth(3);
        square.setHeight(4);
        square.getArea();
    }
}

이 코드의 실행은 문제없이 잘 됩니다. 하지만 LSP를 위배한 코드입니다.

위에서도 말했듯이 단순 컴파일의 성공 실패가 중요한 것이 아니다.

 

하위 클래스의 인스턴스를 상위형 객체 참조 변수에 대입하는 것은 가능 하지만 그 역할을 잘 하고 있는가?

 

Rectangle 클래스에서의 area의 동작은 넓이와 높이를 곱하는 것이다. 그렇게 정의한 area의 기능대로 square 는 3 * 4 = 12 로 동작하기를 기대하지만 결과는 16이 나와 버리기 때문에 LSP를 지켰다고 할 수 없다.

하위타입에서 상위타입과 동일하게 동작하지 않기 때문입니다.

 

그렇다면 어떻게 이 코드를 LSP를 지키게 할 수 있을까요?

package SOLID;

public class LSPExam {

    interface Shape {
        int getArea();
    }

    static class Rectangle implements Shape{
        private int width;
        private int height;

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

        @Override
        public int getArea() {
            return this.width * this.height;
        }
    }

    static class Square implements Shape{
        private int line;

        public Square(int line) {
            this.line = line;
        }

        @Override
        public int getArea() {
            return this.line * this.line;
        }
    }

    public static void main(String[] args) {
        Shape rectangle = new Rectangle(3,4);
        rectangle.getArea();

        Shape square = new Square(4);
        square.getArea();
    }
}

 

생성되는 시점에 사이즈가 결정되게 만들고 인터페이스를 활용하면 넓이를 얻는 기능이 하위타입에서도 동일하게 동작하기를 기대할 수 있게 코드를 만들 수 있습니다. 

 

 

 

 

 

 

 

참고 - 김영한 스프링 핵심원리 기본편, 위키백과