본문 바로가기
Programming

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

by jewook3617 2021. 10. 11.

이번 글에서는 SOLID 원칙의 L에 해당하는 LSP에 대해 정리해보겠습니다.

 

LSP(Liskov Substitution Principle)

LSP는 리스코프 치환 원칙의 줄임말입니다. 리스코프 치환 원칙이란 상위 타입의 객체를 하위 타입의 객체로 치환해도 코드가 문제없이 동작해야 한다는 원칙입니다.

다시 말하면 하위 타입의 객체가 상위 타입의 객체의 모든 동작을 포함하고 있어야 한다는 의미입니다.

LSP를 위반하는 예시 코드를 보겠습니다.

class Rectangle {
  private width: number;
  private height: number;

  constructor(width: number, height: number) {
    this.width = width;
    this.height = height;
  }

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

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

  public area(): number {
    return this.width * this.height;
  }
}

class Square extends Rectangle {
  constructor(width: number, height: number) {
    super(width, height);
  }

  public setWidth(width: number): void {
    this.width = width;
    this.height = width;
  }

  public setHeight(height: number): void {
    this.width = height;
    this.height = height;
  }

  public area(): number {
    return this.width * this.height;
  }
}

const jonyRectangle = new Rectangle(3, 5);
jonyRectangle.setWidth(5);
jonyRectangle.setHeight(3);
jonyRectangle.area() === 15; // true

const jewookRectangle = new Square(3, 3);
jewookRectangle.setWidth(5);
jewookRectangle.setHeight(3);
jewookRectangle.area() === 15; // false

상위 클래스인 Rectangle 클래스가 있고 이를 상속받는 Square 클래스가 있습니다. 

정사각형의 특성 상 Square 클래스의 setWidth(), setHeight() 메서드를 실행하면 width와 height 값이 동시에 바뀌게 됩니다.

그래서 Rectangle 클래스가 사용되던 곳에 Square 클래스를 사용하면 코드가 정상적으로 동작하지 않게 됩니다.

이런 경우 Square 클래스는 Rectangle 클래스의 하위 타입이 될 수 없고 LSP를 위반하게 됩니다.

 

해결 방법

위의 예시 코드가 LSP를 지키도록 하려면 어떻게 해야 할까요?

상속관계를 없애면 됩니다. LSP가 지켜지지 않았다는 것은 상속관계를 잘못 설정했다는 것을 의미합니다.

수학적으로는 직사각형이 정사각형을 포함하고 있어 얼핏 보면 올바른 상속관계처럼 보입니다.

하지만 코드상으로 구현하면 setWidth, setHeight처럼 직사각형과 정사각형이 다르게 동작해야 하는 경우가 생기기 때문에 Square 클래스가 Rectangle 클래스를 상속받는 것은 옳지 않습니다.

이 경우에는 상속관계를 제거하고 동일한 인터페이스를 구현하도록 해야 합니다.

예시 코드를 보겠습니다.

interface Shape {
  area(): number;
}

class Rectangle implements Shape {
  private width: number;
  private height: number;

  constructor(width: number, height: number) {
    this.width = width;
    this.height = height;
  }

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

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

  public area(): number {
    return this.width * this.height;
  }
}

class Square implements Shape {
  private side: number;

  constructor(side: number) {
    this.side = side;
  }

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

  public area(): number {
    return this.side * this.side;
  }
}

const jonyShape: Shape = new Rectangle(2, 8);
jonyShape.area() === 16; // true

const jewookShape: Shape = new Square(5);
jewookShape.area() === 25; // true

이번에는 Rectangle 클래스와 Square 클래스가 상속관계가 아닌 각각 Shape 인터페이스를 구현하고 있습니다. 이렇게 하면 서로 다른 동작을 하는 두 클래스를 서로 치환하려고 하는 불상사를 막을 수 있습니다.

이제는 Rectangle 클래스와 Square 클래스가 각각 Shape 인터페이스의 하위 타입이 되었고 LSP를 지키는 코드가 되었습니다.