250x250
Notice
Recent Posts
Recent Comments
관리 메뉴

탁월함은 어떻게 나오는가?

[SOLID] LSP(리스코브 치환의 원칙: Liskov Subsitution Principle) 본문

[Snow-ball]server/객체지향

[SOLID] LSP(리스코브 치환의 원칙: Liskov Subsitution Principle)

Snow-ball 2023. 8. 8. 15:53
반응형

정의

리스코브 치환의 원칙은 줄여서 LSP라고 부른다. LSP를 한마디로 한다면 "서브 타입은 언제나 슈퍼 타입으로 교체할 수 있어야 한다." 라고 할 수 있다. 즉, 서브 타입은 언제나 상위인 슈퍼 타입과 호환될 수 있어야 한다. 달리 말하면 서브 타입은 슈퍼 타입이 규정한 규약을 전부 지켜야 한다. 상속은 구현상속(inheritance), 인터페이스 상속(implements) 중 어떤걸 사용더라도 궁극적으로 다형성을 통한 확장성 획득이 목표이다. LSP 원리 또한 서브 클래스가 확장에 대한 인터페이스를 준수해야만 함을 의미하고 있다.

 

한줄로 정리하자면, LSP는 application program을 중단하지 않고 슈퍼 클래스의 개체를 해당 하위 클래스의 개체로 교체될 수 있다는 것이다. 말로는 헷갈릴 수 있으니 간단한 예제로 확인해 보자

 

 

 


 

 

 

코드

우리는 얼핏 생각해보면 직사각형을 정사각형에다가 상속을 해줄 수 있지 않을까? 라는 생각을 해볼 수 있다. 그 이유는 정사각형은 직사각형의 한 종류이기 때문에 들 수 있는 생각이다.

 

하지만 이것은 LSP를 위배하는 행위이다. 어떻게 그렇게 되는지 직접적으로 확인을 해보자.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Rectangle {
  private width: number;
  private height: number;
 
  constructor(params: { width: number; height: number }) {
    this.width = params.width;
    this.height = params.height;
  }
 
  public set _width(param: { width: number }) {
    this.width = param.width;
  }
  public get _width(): { width: number } {
    return {
      width: this.width,
    };
  }
 
  public set _height(param: { height: number }) {
    this.height = param.height;
  }
  public get _height(): { height: number } {
    return {
      height: this.height,
    };
  }
 
  public get _area(): { area: number } {
    return {
      area: this.height * this.width,
    };
  }
}
cs

 

Rectangle 클래스를 Squere 클래스에 상속시켜주자.

1
2
3
4
5
class Square extends Rectangle {
  constructor(param: { length: number }) {
    super({ width: param.length, height: param.length });
  }
}
cs

 

 

정삭각형 클래스를 설명하자면, 정사각형의 계산 방식은 width와 height 가 동일한 값으로 곱해줘야 한다. 그래서 length라는 parameter를 한개만 받으면된다. 그래서 width와 height를 동일한 값을 넣어주기 위해 위에처럼 넣어줄 수 밖에 없다.

 

결과는?

1
2
3
4
5
const rectangle: Rectangle = new Rectangle({ width: 10, height: 5 });
console.log(rectangle._area); // { area: 50 }
 
const square: Square = new Square({length105});
console.log(square._area); // Error
cs

 

위의 코드에서 확인해 볼 수 있듯이 에러를 뱉게 된다. 리스코프 치환 원칙에 의하면, 자식 클래스를 부모 클래스가 완전히 대체할 수 있어야 하기때문에 Squere Class 를 Rectangle Class가 완전히 대체되어야 하지만, 불가능하기 때문에 위배되는 것이다.

 

 

 


 

 

 

LSP(Liskov Substitution Principle)에 맞게 개선할 수 있는 방법은?

개선할 수 있는 방법은 두개의 클래스를 따로 두고 하나의 추상화 되있는 클래스 또는 인터페이스를 생성하면 된다.

Shape라는 class 와 interface로 적용하는 사례를 확인해보자.

 

 

클래스 사용시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
class Shape {
  private width: number;
  private height: number;
 
  constructor(params: { width: number; height: number }) {
    this.width = params.width;
    this.height = params.height;
  }
 
  public set _width(param: { width: number }) {
    this.width = param.width;
  }
  public get _width(): { width: number } {
    return {
      width: this.width,
    };
  }
 
  public set _height(param: { height: number }) {
    this.height = param.height;
  }
  public get _height(): { height: number } {
    return {
      height: this.height,
    };
  }
 
  public get _area(): { area: number } {
    return {
      area: this.height * this.width,
    };
  }
}
 
class Rectangle extends Shape {
  constructor(params: { width: number; height: number }) {
    super({ width: params.width, height: params.height });
  }
}
 
class Square extends Shape {
  constructor(param: { length: number }) {
    super({ width: param.length, height: param.length });
  }
}
 
const rectangle: Rectangle = new Rectangle({ height: 10, width: 5 });
console.log(rectangle._area); // { area: 50 }
 
const square: Square = new Square({ length10 });
console.log(square._area); // { area: 100 }
cs

 

 

 

인터페이스 사용시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
interface Shape {
  readonly setWidth: (param: { width: number }) => void;
  readonly setHeight: (param: { height: number }) => void;
  readonly getArea: () => number;
}
 
class Rectangle implements Shape {
  private width: number;
  private height: number;
 
  constructor(params: { width: number; height: number }) {
    this.width = params.width;
    this.height = params.height;
  }
 
  public setHeight(param: { height: number }): void {
    this.height = param.height;
  }
 
  public setWidth(param: { width: number }): void {
    this.width = param.width;
  }
  public getArea(): number {
    return this.width * this.height;
  }
}
 
class Square implements Shape {
  private width: number;
  private height: number;
 
  constructor(param: { length: number }) {
    this.width = param.length;
    this.height = param.length;
  }
 
  public setHeight(param: { height: number }): void {
    this.height = param.height;
  }
 
  public setWidth(param: { width: number }): void {
    this.width = param.width;
  }
 
  public getArea(): number {
    return this.height * this.width;
  }
}
 
const rectangle: Rectangle = new Rectangle({ width: 10, height: 5 });
console.log(rectangle.getArea()); // { area: 50 }
 
const square: Square = new Square({ length10 });
console.log(square.getArea()); // { area: 100 }
cs

 

 

 


 

 

 

결론

리스코프 치환의 원칙은 인터페이스, 클래스 어떤걸 사용해도 상관은 없다. 하지만, 클래스의 경우 오버라이딩이 가능해지기 때문에 잘모르고 사용한다면 리스코프 치환의 원칙을 어길 수 있을 가능성이 생긴다. 물론, 필요에 의해서 적용 안할 수 있는 경우를 제외하고는 인터페이스로 강제하는게 제일 효율적일 것이라는 생각이 든다.

 

 

 

 

 

reference

* What Is Liskov Substitution Principle (LSP)? With Real World Examples! 

* 리스코프 치환 원칙 - Wiki

 

 

 

반응형
Comments