Post

SOLID 디자인 원칙3 : LSP

전체 SOLID 디자인 원칙의 링크들

  1. SRP : 단일 책임 원칙
  2. OCP : 열림-닫힘 원칙
  3. LSP : 리스코프 치환 원칙
  4. ISP : 인터페이스 분리 원칙
  5. DIP : 의존성 역전 원칙

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를 할 때 생각하면서 코드를 짜자.

This post is licensed under CC BY 4.0 by the author.