SOLID에서 L에 해당되는 Liskov Substitution Principle(LSP)이다. 이 원칙은 서브 타입은 기반 타입을 대체할 수 있다는 법칙이다. 즉, 부모 클래스 A를 상속한 클래스 B는 언제든 A클래스를 대체할 수 있다는 뜻이다. 이것은 다형성(polymorphism)이 의미하는 것과 비슷해 보인다. 단순히 대체할 수 있는 것은 아니고, 기반 타입과 서브 타입은 서로 제한된 형태에서만 대체 가능하게 된다.
💡 요구사항
- 하위형에서 메서드 인수의 반공변성
- 하위형에서 반환형의 공변성
- 하위형에서 메서드는 상위형 메서드에서 던져진 예외의 하위형을 제외하고 새로운 예외를 던지면 안된다.
💡 하위형이 만족해야 하는 행동 조건
- 하위형에서 선행조건은 강화될 수 없다.
하위형에서 후행조건은 약화될 수 없다.
하위형에서 상위형의 불변조건은 반드시 유지되어야 한다.
위키피디아
우선 공변성과 반공변성을 이해해보자.
- 공변할 때, 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|number를 A타입이라고 하고 number를 B타입이라고 한다면 타입스크립트의 함수 반환 값은 공변하기 때문에 B<:A를 만족하면 f<B><:f<A>를 만족하게 된다. speakAge에 speak을 할당 수 없지만, speak에 speakAge를 할당할 수 있게 된다.
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>에 할당할 수 있게 된다.
이 조건들은 서브 타입이 지켜야 할 내용이다.
// 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를 아무렇지 않게 서브 클래스가 덮어 씌워 사용하게 된다. 이는 불변 조건을 유지하지 못한다.
위 조건들을 만족해야 기반 타입을 서브 타입으로 대체할 수 있게 된다.
정말 어려운 개념인데 이러한 개념을 요구하는 이유는 역시 유연한 코드를 위해서라고 생각한다. 이번에 정리하며 느낀 점은 유연한 코드를 위해서는 큰 제약이 필요하다는 생각이 든다.
참조