2023년 11월 5일 작성

LSP - Liskov 치환 원칙

하위 유형은 언제나 상위 유형으로 교체할 수 있어야 합니다.

LSP (Liskov Substitution Principle) : Liskov 치환 원칙

  • Liskov 치환 원칙은 sub type(하위 유형)은 언제나 super type(기반이 되는 유형)으로 교체할 수 있어야 한다는 원칙입니다.
    • 1988년 Barbara Liskov가 올바른 상속 관계의 특징을 정의하기 위해 발표했습니다.
  • “교체할 수 있다”는 말은, 자식 class는 최소한 자신의 부모 class에서 가능한 행위는 수행할 수 있어야 한다는 말입니다.
    • 부모 class의 instance를 사용하는 위치에 자식 class의 instance를 대신 사용했을 때, code가 원래의 의도대로 작동해야 합니다.
    • 이것을 부모 class와 자식 class 사이의 행위가 일관성이 있다고 말한다.
  • LSP는 다형성을 지원하기 위한 원칙입니다.
    • 다형성 기능을 이용하기 위해서는 class를 상속시켜 type을 통합할 수 있게 설정하고, upcasting을 해도 method 동작에 문제가 없도록 설계하여야 하기 때문입니다.

Java Collection Framework : LSP를 잘 지킨 사례

  • 변수에 LinkedList 자료형을 담아 사용하다가, 중간에 전혀 다른 HashSet 자료형으로 바꿔도, add() method 동작을 보장받기 위해서는 Collection이라는 interface type으로 변수를 선언하여 할당하면 됩니다.
  • interface Collection의 추상 method를 각기 하위 자료형 class에서 implements하여 interface 구현 규약을 잘 지키도록 미리 잘 설계되어 있기 때문입니다.
classDiagram

class Collection

class List
class Set

class Vector
class Stack
class ArrayList
class LinkedList

class StoredSet
class HashSet

Collection <|-- List
Collection <|-- Set
List <|-- Vector
List <|-- Stack
List <|-- ArrayList
List <|-- LinkedList
Set <|-- StoredSet
Set <|-- HashSet
void myData() {
    Collection data = new LinkedList();    // Collection interface type으로 변수를 선언합니다.
    data = new HashSet();    // 중간에 전혀 다른 자료형 class를 할당해도 호환이 됩니다.
    
    modify(data);
}

void modify(Collection data){
    list.add(1);    // interface 구현 구조가 잘 잡혀있기 때문에 add method 동작이 각기 자료형에 맞게 보장됩니다.
    // ...
}

LSP 적용해보기

적용 전

  • Rectangle은 직사각형을 구현한 객체입니다.
    • 너비와 높이를 지정하고 반환할 수 있으며, 지정된 값을 통해 자신의 넓이를 계산할 수 있습니다.
  • 이때, 직사각형을 상속하여 정사각형을 만들면 Liskov 치환 원칙에 위배됩니다.
    • 정사각형 역시 직사각형의 한 종류입니다.
    • 하지만 정사각형은 직사각형과 달리 너비와 높이가 같기 때문에 너비나 높이를 지정하면 그에 맞게 너비와 높이를 모두 동일한 값으로 지정하게 됩니다.
    • 그래서 Square로 넓이를 계산했을 때와 Rectangle로 넓이를 계산했을 때는 다른 결과가 나옵니다.
    • SquareRectangle을 완전히 대체할 수 없기 때문에 Liskov 치환 원칙을 위배합니다.
  • 직사각형과 정사각형은 상속 관계가 될 수 없습니다.
    • 사각형의 특징을 서로 가지고 있긴 하지만, 두 사각형 모두 사각형의 한 종류일 뿐입니다.
    • 하나가 다른 하나를 완전히 포함하지 못하는 구조입니다.
  • 잘못된 객체를 상속하거나, 올바르게 확장하지 못한 경우에 Liskov 치환 원칙을 위반하기 쉽습니다.
// 직사각형 class
public class Rectangle {
    protected int width;
    protected int 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;
    }
    
    public int getArea() {
        return width * height;
    }
}

// 정사각형 class
public class Square extends Rectangle {
    @Override
    public void setWidth(int width) {
        super.setWidth(width);
        super.setHeight(getWidth());
    }
    
    @Override
    public void setHeight(int height) {
        super.setHeight(height);
        super.setWidth(getHeight());
    }
}

// main class
public class Main {
    public static void main(String[] args) {
        Rectangle rectangle = new Rectangle();
        rectangle.setWidth(10);
        rectangle.setHeight(5);
        System.out.println(rectangle.getArea());    // 50
        
        Rectangle square = new Square();
        square.setWidth(10);
        square.setHeight(5);
        System.out.println(square.getArea());    // 125
    }
}

적용 후

  • 직사각형과 정사각형은 상속의 관계가 성립되기 어렵습니다.
  • 따라서 정사각형(Square)이 직사각형(Rectangle)를 상속받지 않고, 더 상위 개념인 사각형 객체(Shape)를 상속받도록 수정하여, Liskov 치환 원칙의 영향에서 벗어납니다.

  • 이 예제는 Liskov 치환 원칙에 ‘위배되지 않도록’ 수정한 것입니다.
    • Liskov 치환 원칙을 ‘준수하는’ 예제로는 Java Collection Framework가 있습니다.
// 사각형 객체
public class Shape {
    protected int width;
    protected int 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;
    }
    
    public int getArea() {
        return width * height;
    }
}

// 직사각형 class
class Rectangle extends Shape {
    public Rectangle(int width, int height) {
        setWidth(width);
        setHeight(height);
    }
}

// 정사각형 class
class Square extends Shape {
    public Square(int length) {
        setWidth(length);
        setHeight(length);
    }
}

// 메인 class
public class Main {
    public static void main(String[] args) {
        Shape rectangle = new Rectangle(10, 5);
        Shape square = new Square(5);

        System.out.println(rectangle.getArea());    // 50
        System.out.println(square.getArea());    // 25
    }
}

LSP 적용 주의 사항

  • LSP 원칙의 핵심은 상속(inheritance)입니다.
    • 다형성의 특징을 이용하기 위해 상위 class type으로 객체를 선언하여 하위 class의 instance를 받으면, upcasting된 상태에서 부모의 method를 사용해도 의도한대로만 수행되어야 한다는 원칙입니다.
  • 하지만 객체 지향 programming에서 상속의 사용은 특별한 상황이 아니면 권장되지 않습니다.
    • 기반 class와 sub class 사이에 확실한 IS-A 관계가 있는 경우에만 상속을 사용해야 합니다.
    • 대부분의 경우는 합성(composition)을 사용하는 것이 더 낫습니다.
  • 따라서 다형성을 이용하고 싶다면, 상속(extends) 대신 insterface로 구현(implements)하여 interface type으로 사용하는 것을 더 권장합니다.
  • 또한 상위 class의 기능을 이용하거나 재사용하고 싶다면, 상속(inheritnace)보다는 합성(composition)으로 구성하는 것이 대부분 더 낫습니다.

Reference


목차