[SOLID] 리스코프 치환 원칙 (Liskov Substitution Principle, LSP)

엔스마트·2024년 6월 7일

리스코프 치환 원칙 (Liskov Substitution Principle, LSP)은 객체 지향 설계 원칙 중 하나로, 바바라 리스코프(Barbara Liskov)가 1987년에 제안했습니다. 이 원칙은 상위 타입의 객체를 하위 타입의 객체로 치환해도 프로그램의 동작에 이상이 없어야 한다는 것을 의미합니다. 즉, 자식 클래스는 언제나 부모 클래스로 대체할 수 있어야 하며, 이는 자식 클래스가 부모 클래스의 계약을 완전히 준수해야 함을 뜻합니다.

리스코프 치환 원칙(LSP)은 상위 타입과 하위 타입 간의 계약을 통해 상속 관계의 일관성과 신뢰성을 보장하는 중요한 원칙입니다. 이를 준수하면 코드의 유연성과 확장성이 향상되며, 유지보수와 테스트가 용이해집니다. LSP를 염두에 두고 설계함으로써 더 견고하고 재사용 가능한 객체 지향 소프트웨어를 개발할 수 있습니다.

LSP의 중요성

  1. 코드의 신뢰성을 보장합니다:
    LSP를 준수하면, 상위 타입을 사용하는 코드가 하위 타입에 대해서도 동일하게 작동할 수 있기 때문에, 코드의 신뢰성과 일관성을 유지할 수 있습니다.

  2. 유연성과 확장성을 제공합니다:
    하위 타입이 상위 타입을 완벽하게 대체할 수 있으므로, 코드의 유연성과 확장성을 높일 수 있습니다. 새로운 하위 클래스를 추가할 때 기존 코드를 수정하지 않아도 됩니다.

  3. 테스트 용이성:
    LSP를 준수하는 코드는 테스트가 더 용이합니다. 상위 클래스의 테스트 케이스를 하위 클래스에서도 동일하게 적용할 수 있기 때문입니다.

LSP를 준수하는 방법

  1. 상위 클래스의 계약을 준수합니다:
    하위 클래스는 상위 클래스가 제공하는 모든 메서드와 속성의 계약(즉, 동작, 예외 처리, 반환 값 등)을 준수해야 합니다.

  2. 기존 기능을 변경하지 않습니다:
    하위 클래스는 상위 클래스의 기능을 변경해서는 안 됩니다. 대신, 새로운 기능을 추가하되, 상위 클래스의 동작을 유지해야 합니다.

  3. 예외 상황 처리 일관성 유지:
    하위 클래스는 상위 클래스에서 정의된 예외 상황 처리를 변경하지 않고 일관되게 유지해야 합니다.

예시

예를 들어, 도형의 넓이를 계산하는 프로그램을 생각해봅시다. Rectangle 클래스를 상속받는 Square 클래스를 구현한다고 가정합니다.

잘못된 예시:

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;
    }
}

class Square extends Rectangle {
    @Override
    public void setWidth(int width) {
        this.width = width;
        this.height = width; // 정사각형이므로 너비와 높이가 같아야 함
    }

    @Override
    public void setHeight(int height) {
        this.height = height;
        this.width = height; // 정사각형이므로 너비와 높이가 같아야 함
    }
}

위 예시에서는 Square 클래스가 Rectangle 클래스를 상속받았지만, setWidthsetHeight 메서드를 재정의하여 너비와 높이를 동시에 변경하고 있습니다. 이는 Rectangle 클래스의 계약을 어기는 것입니다. Rectangle 클래스는 너비와 높이를 독립적으로 설정할 수 있어야 하지만, Square 클래스에서는 이를 불가능하게 만듭니다.

LSP를 준수하는 예시:

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 int getWidth() {
        return width;
    }

    public int getHeight() {
        return 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 int getSide() {
        return side;
    }

    public void setSide(int side) {
        this.side = side;
    }

    @Override
    public int getArea() {
        return side * side;
    }
}

이 예시에서는 Shape 인터페이스를 도입하여 RectangleSquare 클래스가 각각 독립적으로 구현됩니다. Square 클래스는 더 이상 Rectangle을 상속받지 않으며, 각 클래스는 자신의 책임을 명확히 하고, 상위 타입의 계약을 준수합니다.

profile
클라우드 전환, MSA 서비스, DevOps 환경 구축과 기술지원 그리고 엔터프라이즈 시스템을 구축하는 최고 실력과 경험을 가진 Architect Group 입니다.

0개의 댓글