본 글은 Steven Giesel님의 Is a square a rectangle? Liskov substitution principle in action 글을 번역하였습니다.
이 간단한 질문을 던져보겠습니다: “정사각형은 직사각형인가요?” 그러면 여러분은 감사할 것입니다: “그럼요, 물론이죠!”
하지만 잠시만 기다려서 함께 확인해 봅시다. SOLID: 리스코프 치환 원칙의 L을 사용하여 이 관계가 맞는지 확인해 보겠습니다!
SOLID로 플래시백
저는 SOLID 원칙에 대한 전체 기사를 작성했습니다: “C#의 SOLID 원칙” 그래서 여기서는 그 이유에 대해서만 설명하겠습니다: 리스코프 대체 원칙(LSP)은 다음과 같이 요약할 수 있습니다. 어떤 클래스가 다른 클래스의 서브클래스인 경우, 코드에서 문제를 일으키지 않고 슈퍼클래스(또는 인터페이스)를 대체할 수 있어야 한다는 것입니다. 기본적으로 우리는 서브클래스의 객체가 슈퍼클래스(또는 인터페이스)의 객체와 동일한 방식으로 동작하기를 원합니다.
이제 물어볼 수 있습니다:
정사각형은 직사각형인가요?
그렇다면 정사각형은 직사각형일까요? 정답은 '예’와 '아니오’입니다. 예, 정사각형은 직사각형의 한 유형이며 직사각형의 모든 속성을 상속 받기 때문입니다. 그러나 정사각형에는 직사각형에 적용되지 않는 추가적인 제약 조건이 있기 때문에 정사각형은 직사각형이 아닙니다. 정사각형은 네 변이 같지만 직사각형은 모든 변의 조합을 가질 수 있습니다. 다음 코드를 살펴보세요:
public class Rectangle
{
public int Width { get; private set; }
public int Height { get; private set; }
public int Area => Width * Height;
public virtual void SetWidth(int width) => Width = width;
public virtual void SetHeight(int height) => Height = height;
}
public class Square : Rectangle
{
public override void SetWidth(int width) => (Width, Height) = (width, width);
public override void SetHeight(int heigth) => (Width, Height) = (heigth, heigth);
}
언뜻 보기에는 간단해 보입니다. 이제 제공된 코드를 살펴보겠습니다. 이 코드는 Square 클래스가 Rectangle 클래스의 SetWidth
및 SetHeight
메서드의 동작을 수정하기 때문에 리스코프 치환 원칙을 위반합니다. Rectangle 클래스에서 SetWidth
및 SetHeight
메서드는 직사각형의 너비와 높이를 독립적으로 설정할 수 있습니다. 그러나 Square 클래스에서는 두 메서드 모두 너비와 높이를 동일한 값으로 설정합니다.
즉, Square 클래스의 인스턴스를 생성하여 Rectangle 인스턴스 대신 사용하면 예기치 않은 결과가 나올 수 있습니다. 예를 들어, 주어진 테스트 사례에서는 너비를 10으로 설정하고 높이를 5로 설정했습니다. 그러나 Square 인스턴스를 사용하기 때문에 너비와 높이가 모두 10으로 설정되고 그 이후에는 5로 설정되어 “일반” 직사각형의 경우 면적이 50이 아닌 100이 되고 25가 됩니다.
public class Tests
{
[Fact]
public void GivenSquareRectangle_CalculateArea()
{
Rectangle rectangle = new Square();
rectangle.SetWidth(10);
rectangle.SetHeigth(5);
// This will fail - but if we would adhere to
// The Liskov substitution principle that shouldn't happen
rectangle.Area.Should.Be(50);
}
}
결론
리스코프 대체 원칙은 객체 지향 프로그래밍에서 매우 중요한 원칙이며, 이를 위반하면 프로그램에서 예기치 않은 동작이 발생할 수 있습니다. 우리는 이 원칙을 숙지하고 코드가 이 원칙을 따르도록 하여 프로그램을 더욱 유지보수 가능하고 예측 가능하게 만들어야 합니다.