Liskov Substitution Principle(LSP)

CheolHyeon Park·2022년 12월 27일

SOLID에서 L에 해당되는 Liskov Substitution Principle(LSP)이다. 이 원칙은 서브 타입은 기반 타입을 대체할 수 있다는 법칙이다. 즉, 부모 클래스 A를 상속한 클래스 B는 언제든 A클래스를 대체할 수 있다는 뜻이다. 이것은 다형성(polymorphism)이 의미하는 것과 비슷해 보인다. 단순히 대체할 수 있는 것은 아니고, 기반 타입과 서브 타입은 서로 제한된 형태에서만 대체 가능하게 된다.

LSP를 만족하기 위한 조건

💡 요구사항

  • 하위형에서 메서드 인수의 반공변성
  • 하위형에서 반환형의 공변성
  • 하위형에서 메서드는 상위형 메서드에서 던져진 예외의 하위형을 제외하고 새로운 예외를 던지면 안된다.

💡 하위형이 만족해야 하는 행동 조건

  • 하위형에서 선행조건은 강화될 수 없다.
    하위형에서 후행조건은 약화될 수 없다.
    하위형에서 상위형의 불변조건은 반드시 유지되어야 한다.
    위키피디아

우선 공변성과 반공변성을 이해해보자.

  • 공변할 때, S가 T의 서브타입이라면(S<:T), I<S>I<T>의 서브타입이다.
  • 반공변할 때, S가 T의 서브타입이라면(S<:T), I<T>I<S>의 서브타입이다.

정의는 이렇게 되지만 무슨말인지 모르겠다. 예제를 보면서 알아보자

공변성과 반공변성 코드

  • 공변성
type SpeakAgeOrName<T> = () => T;
let speak: SpeakAgeOrName<string|number> = () => "";
let speakAge: SpeakAgeOrName<number> = () => 30;

speak = speakAge;  // OK
speakAge = speak;  // Error

string|numberA타입이라고 하고 numberB타입이라고 한다면 타입스크립트의 함수 반환 값은 공변하기 때문에 B<:A를 만족하면 f<B><:f<A>를 만족하게 된다. speakAgespeak을 할당 수 없지만, speakspeakAge를 할당할 수 있게 된다.

  • 반공변성
type Logger<T> = (param: T) => void;
let log: Logger<string | number> = (param) => {
  console.log(param);
};
let logNumber: Logger<number> = (param) => {
  console.log(param);
};
log = logNumber; // Error
logNumber = log; // OK

타입스크립트에서 인자는 반공변하기 때문에, 위와 같은 이유로 Logger<string|number>Logger<number>에 할당할 수 있게 된다.

선행조건 강화 X, 후행 조건 약화 X, 불변 조건 유지

이 조건들은 서브 타입이 지켜야 할 내용이다.

  • 선행조건 강화는 서브 클래스에 기반 클래스의 조건을 덧붙이는 것이다.
// super class
method(num) {
  if(num > 0) {
   // blah, blah 
  }
}

// sub class
method(num) {
  if(num >= 0) {
   // blah, blah 
  }
}

위와 같이 super class의 조건에 0을 추가했다. 이는 기반 클래스를 더 까다롭게 강화하는 것에 해당되어 LSP를 위반한다.

  • 후행조건 약화는 서브 클래스에서 기반 클래스의 조건을 약화 시키는 것을 의미한다.
// super class
method_A(int data)
{
    int result;
    
    // 실행 코드
    
    if (result < 0)
    {
    	result = 0;
    }
    
    return result;
}
// sub class
method_A(int data) {
  int result;
  
  // 실행 코드
  
  return result;
}

위 코드는 기반 클래스의 음수 조건을 제거(약화)하여 서브 클래스에서 사용하고 있다. 이렇게 되면 대체했을 때 음수 조건으로 인해 서브 클래스가 기반 클래스를 대체할 수 없게 된다.

  • 불변 조건 유지는 기반 클래스에서 유지되던 데이터의 불변성을 유지해야 하는 조건이다.
class Parent {
  _data;
  get() {
    return this._data;
  }
  set(value) {
    if (value < 0) {
      this._data = 0;
      return;
    }

    this._data = value;
  }
}

class Child extends Parent {
  constructor() {
    super();
  }

  method(data) {
    this._data = data;
  }
}

const par = new Parent();
const chid = new Child();
chid.set("20");

위 코드는 기반 클래스의 _data를 아무렇지 않게 서브 클래스가 덮어 씌워 사용하게 된다. 이는 불변 조건을 유지하지 못한다.

위 조건들을 만족해야 기반 타입을 서브 타입으로 대체할 수 있게 된다.

정리

정말 어려운 개념인데 이러한 개념을 요구하는 이유는 역시 유연한 코드를 위해서라고 생각한다. 이번에 정리하며 느낀 점은 유연한 코드를 위해서는 큰 제약이 필요하다는 생각이 든다.

참조

profile
나무아래에 앉아, 코딩하는 개발자가 되고 싶은 박철현 블로그입니다.

0개의 댓글