SOLID의 세 번째 원칙인 리스코프 치환 원칙Liskov substitution principle에 대해 알아보도록 한다.

리스코프 치환 원칙이란?

“FUNCTIONS THAT USE POINTERS OR REFERENCES TO BASE CLASSES MUST BE ABLE TO USE OBJECTS OF DERIVED CLASSES WITHOUT KNOWING IT.”

Robert Martin. LSP: The Liskov Substitution Principle

기반 클래스base class의 사용자는 파생 클래스derived class의 존재를 몰라도 기반 클래스를 사용할 수 있어야 한다. LSP를 위반한다는 것은 기반 클래스를 참조하면서 파생 클래스들에 대해서도 알고 있는 것이다. 이런 코드는 파생 클래스들이 추가될 때 마다 수정될 가능성이 높다. 즉, 수정에 닫혀 있지 못한 OCP 위반이 된다.

이를 설명하는 여러 예제가 있지만 사각형과 정사각형 이야기가 가장 흥미롭다. 아래 그림을 먼저 보도록 하자.

two

그림 1. 사각형을 상속하는 정사각형

‘정사각형은 사각형이다’라고 할 수 있으므로 ISA 관계가 성립하는 것 처럼 보인다. 따라서 그림의 상속 관계도 자연스러워 보인다. 하지만 코드와 함께 살펴 보면 이상한 부분들이 보이기 시작한다.

public class Rectangle {

   @Getter
   @Setter
   private double width;

   @Getter
   @Setter
   private double height;

}

public class Square extends Rectangle {}

위 코드는 넓이와 높이 속성을 가진 RectangleSquare가 상속하도록 하고 있다. 그런데, Square가 상속하는 속성과 행위가 모두 필요한 걸까? 정사각형은 넓이와 높이가 동일하기 때문에 이는 생각해 볼 일이다. 우선 메모리 사용 측면에서 비효율이다. Square 인스턴스가 생성될 때 마다 매번 2개 필드를 위한 메모리가 사용되기 때문이다. 두 번째로, 코드를 읽는 사람에게 혼란을 야기할 수 있다. “정사각형은 가로 세로가 동일한데 왜 필드가 2개이고 넓이와 높이를 지정하는 메서드가 각각 존재하는 건가?” 자칫 widthheight가 서로 달라지는 오류를 범할 수도 있다. setWidthsetHeight를 오버라이드 해서 각 메서드 호출 시 마다 widthheight를 항상 동일하게 맞춰준다고 해도 코드의 사용자에게 주는 혼란은 여전하다. 아래 코드 처럼 말이다.

public void inconsistency(Rectangle rectangle) {
   rectangle.setHeight(10);
   rectangle.setWidth(5);
   double area = rectangle.getHeight() * rectangle.getWidth();
   assert area == 50;
}

메모리 비효율과 읽기 혼란을 무시한다고 해도 다른 문제가 남아 있다. Rectangle의 사용자가 Square의 존재를 알아야 한다는 것이다. 이 경우 다음과 같은 코드가 만들어 질 수 있다.

public void ocpViolation(Rectangle rectangle) {
   rectangle.setHeight(10);
   rectangle.setWidth(5);

   double area = rectangle.getHeight() * rectangle.getWidth();
   if (rectangle instanceof Square) {
       assert area == 100;
   } else {
       assert area == 50;
   }
}

기존의 inconsistency 메서드는 Rectangleㅇ 객체를 참조하고 있지만 이 객체가 Square(Rectangle의 파생 클래스)의 객체일 때는 기대한 대로 동작하지 않는다. 그래서 ocpViolation 메서드 처럼 별도의 처리가 필요하게 되고, 결국 ‘기반 클래스를 참조하면서 파생 클래스들에 대해서도 알고 있는’ LSP 위반이 된다. 이런 식의 Rectangle 파생 클래스가 추가될 때 마다 ocpViolation도 함께 수정해 주어야 하므로 OCP를 위반하게 된다.

파생 클래스들은 언제든지 기반 클래스로 치환substitution 될 수 있어야 하며, 그러기 위해서 모든 파생 클래스들은 기반 클래스의 클라이언트 코드들이 기반 클래스에게 기대하는 것을 반드시 따라야만 한다. 이를 통해 기반 클래스들에 의존하는 코드들이 재사용 가능해지고 파생 클래스들의 변경에 영향 받지 않을 수 있게 된다. 이는 OCP가 추구하는 바 그대로이며, 이런 측면에서 LSP는 OCP를 따르기 위한 수단으로 보여지기도 한다.

*작성된 코드 전체는 여기서 확인할 수 있습니다.

정사각형은 사각형인가?

정사각형은 사각형이 맞다. 하지만 SquareRectangle 클래스는 ISA 관계가 성립하지 않는다. Square 객체의 행위가 Rectangle의 그것과 상이하기 때문이다. 상태와 행위를 가지는 것이 객체이고 클래스는 이 객체의 행위를 구현한 템플릿template 임을 생각할 때 SquareRectangle이 아니다.

참고 문헌

[1] Robert Martin. LSP: The Liskov Substitution Principle.
[2] What is Object?
[3] What is Class?

관련 문서

[1] SOLID 개요
[2] 단일 책임 원칙 SRP
[3] 개방 폐쇄 법칙 OCP
[4] 리스코프 치환 법칙 LSP