SOLID 디자인 원칙3 : LSP
전체 SOLID 디자인 원칙의 링크들
SOLID 디자인 원칙의 세 번째, 리스코프 치환 원칙(Liskov Subsitution Principle, LSP)
리스코프 치환 원칙은 어떤 자식 객체에 접근할 때 그 부모 객체의 인터페이스로 접근하더라도 아무런 문제가 없어야 한다는 것을 의미한다.
즉, 자식객체를 그 부모 객체와 동등하게 취급할 수 있어야 한다.
먼저 LSP가 준수되지 않는 예시를 알아보자.
아래는 직사각형 클래스이다. 이 클래스는 가로/세로 길이에 대한 get/set 및 면적 계산을 위한 멤버 함수를 가진다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Rectangle {
protected:
int width, height;
public:
Rectangle(const int _width, const int _height)
: width(_width), height(_height)
{}
int get_width() const { return width; };
virtual void set_width(const int _width) { width = _width; }
int get_height() const { return height; }
virtual void set_height(const int _height) { height = _height; }
int area()const { return width * height; }
};
이제 직사각형의 특별한 경우인 정사각형을 만들어 보자. 이 객체는 가로/세로 get/set 멤버 함수를 모두 오버라이딩한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Square
:public Rectangle
{
public:
Square(const int _size)
: Rectangle(_size, _size)
{}
void set_width(const int _width) override {
width = height = _width;
}
void set_height(const int _height) override {
height = width = _height;
}
};
언뜻 보기에는 해로울 것이 전혀 없어 보인다. 하지만 이러한 접근 방법은 문제를 일으킨다. 단지 멤버 함수 set에서 가로세로값 모두를 설정할 뿐인데, 이 객체를 그 부모인 Rectangle 객체로서 접근하면 의도치 않은 상황이 생긴다.
1
2
3
4
5
6
void process(Rectangle& _r) {
int w = _r.get_width();
_r.set_height(10);
cout << "기대된 값 : " << (w * 10) << ", 얻은 값 : " << _r.area() << "\n";
}
가로 길이를 가져오고 세로를 10으로 설정하고, 가져온 가로 길이에 상수 10을 곱하여 넓이를 구하고 있다. 이 코드만 볼 때는 계산된 넓이가 틀릴 것 같지 않다. 하지만 Square 객체를 인자로 하여 이 함수를 호출하면 엉뚱한 넓이가 계산된다.
1
2
Square s(5);
process(s); // 기대된 값 : 50, 얻은 값 : 100
비록 작위적인 예제이지만 여기에서 교훈은 LSP를 준수하지 않으면 파생된 서브 클래스 Square를 그 부모 클래스 Rectangle 타입으로 활용할 때 당장은 괜찮을 수 있어도 나중에 문제가 발견될 수 있다는 것이다. 최악의 경우 제품이 고객에게 큰 문제를 일으키고 나서야 문제를 인지할 수도 있다.
이 문제를 해결하는 여러 가지 방법이 있다. 첫 번째 방법은 애당초 서브 클래스를 만들지 않는 것이다. 서브 클래스를 만드는 대신 아래와 같이 Factory
클래스를 두어 직사각형과 정사각형을 따로따로 생성한다.
1
2
3
4
class RectangleFactory {
static Rectangle create_rectangle(int _w, int _h);
static Rectangle create_square(int _size);
};
정사각형인지 여부를 확인해야 할 수 있다. 이를 위해 아래와 같은 멤버 함수를 둔다.
1
bool is_square() const { return width == height; }
좀 더 아랫단에서의 대중적인 처리 방법도 있다. Square의 멤버 함수 set_width()/set_height()에서 예외를 발생시키고, 그 멤버 함수들 대신 set_size()를 사용하게 하는 것이다.하지만 이 방법은 놀람 최소화 원칙을 위배한다. set_width()의 사용은 분명 자연스러운 일이고 평온해야 할 작업이다. 정상적인 숫자가 파라미터로 넘겨졌음에도 예외가 발생할 거라고 예상하기는 어렵다.
요약
LSP는 어떤 자식 객체에 접근할 때 그 부모 객체의 인터페이스로 접근하더라도 아무런 문제가 없어야 한다는 것을 의미한다 즉, 자식 객체를 그 부모 객체와 동등하게 취급할 수 있어야 한다. override를 할 때 생각하면서 코드를 짜자.