이 글은 객체지향 5대 원칙, SOLID principle 중 L인 Liskov Substition Principle을 다룬다.
정의
subtype이 항상 supertype로 치환할 수 있어야 한다는 원칙이다. 이 원칙은 polymorphism에 관련된 이야기이다. 다르게 설명하자면 supertype을 사용하는 위치에 subtype을 넣어도 프로그램 수행에는 변화가 없어야 한다는 말이다. 따라서 supertype에서 사용하는 method는 subtype에서도 사용할 수 있어야 한다.
polymorphism을 공부했다면 method overriding과 upcasting에 대해 이미 알고 있을 것이다. Liskov Substitution Principle를 따른다면 upcasting한 상태에서 parent class의 method를 사용할 수 있어야 하므로 overriding 시 parent class method의 동작 의도를 크게 수정해서는 안된다.
위배 예시
Liskov Substution Principle을 설명할 때 많이 사용하는 예제로 Rectangle-Square 문제가 있다. Square는 네 각이 모두 같고 네 변의 길이가 같은 사각형이다. Rectangle은 네 각기 모두 같은 사각형이다. 즉 Square는 Rectangle이다(Square is a Rectangle) 라고 표현할 수 있다. 어? is-a 관계? 상속하면 되겠네? 상속해 보자.
// Rectangle.java
public class Rectangle{
private int width;
private int height;
public void setWidth(int width){
this.width = width;
}
public void setHeight(int height){
this.height = height;
}
public int getArea(){
return this.width * this.height;
}
}
// Square.java
public class Square extends Rectangle{
@Override
public void setHeight(int height){
super.setHeight(height);
super.setWidth(height);
}
@Override
public void setWidth(int width){
super.setHeight(width);
super.setWidth(width);
}
}
Square의 경우 height나 width가 바뀌면 height, width 둘 다 수정해 주어야 하기 때문에 위와 같은 코드를 작성했다.
// App.java
public class App {
public static void main(String[] args) throws Exception {
Rectangle rectangle = new Rectangle();
rectangle.setHeight(10);
rectangle.setWidth(5);
System.out.println(rectangle.getArea()); // 50
Rectangle rectangle2 = new Square();
rectangle2.setHeight(10);
rectangle2.setWidth(5);
System.out.println(rectangle2.getArea()); // 25
}
}
subclass인 square로 base class를 치환했고, 똑같은 코드를 작성했지만 결과가 다르다. Liskov Substitution Principle을 위배하게 된다.
이런 현상이 발생하는 이유는 Square class의 setHeight, setWidth method에서 Rectangle class method의 동작 의도를 크게 수정했기 때문이다. Rectangle class에서는 height와 width 각각을 수정한다. 반면 Square class에서는 특성으로 인해 height와 width를 둘 다 수정했다.
Rectangle/Square의 문제는 Rectangle와 Square의 정의는 is-a 관계가 맞지만 실제 구현에 있어서는 is-a 관계로 나타내기 어렵다. Square에서 변의 길이가 바뀌면 width, height가 모두 바뀌어야 하기 때문이다. 따라서 상속이 아닌 다른 방법으로 나타내야 한다.
- Shape라는 공통 상위 interface를 두고, getArea() method를 overriding하는 방법 등이 있을 것이다.
이점
코드 재사용성 증가
Liskov Substitution Principle을 준수하면 supertype의 코드를 재사용할 수 있다.
확장성, 유연성, 유지보수성 증가
Liskov Substitution Principle을 준수하면 subtype이 supertype을 대체할 수 있고 프로그램의 전체 동작이 변하지 않는다. 따라서 subtype을 추가하더라도 기존에 작성된 코드를 수정하지 않고 object를 교체할 수 있으므로 프로그램의 확장성과 유연성이 향상된다.
유의점
Liskov Substitution Principle는 subtype과 supertype의 관계에서 사용되는 원칙이다. 이 경우 상속은 is-a 관계처럼 대체할 수 없는 명확한 관계가 있을 때만 사용해야 한다. 만약 애매한 관계일 때는 상속보다 interface을 이용해 abstraction하거나 composition을 사용하는 것이 더 낫다.
설령 명확한 is-a 관계라서 상속을 했더라도 method overriding 시 parent class method의 동작 의도를 크게 수정해서는 안된다.
'CS > OOP' 카테고리의 다른 글
[OOP] SOLID - Dependency Inversion Principle (0) | 2023.03.12 |
---|---|
[OOP] SOLID - Interface Segregation Principle (0) | 2023.03.11 |
[OOP] SOLID - Open Closed Principle (0) | 2023.03.07 |
[OOP] SOLID - Single Responsibility Principle (0) | 2023.03.06 |
[OOP] 객체지향 프로그래밍 Object-Oriented Programming (0) | 2023.02.17 |