base

리스코프 치환 원칙 (Liskov Substitution Principle, LSP)이란?

개발을 하다 보면 자연스럽게 상속(inheritance)을 자주 사용하게 됩니다. 예를 들어, 일반적인 Animal 클래스가 있고 이를 상속받아 다양한 종류의 동물(Dog, Cat)을 만들 수 있습니다. 여기서 상속이 잘 이루어지면 DogCatAnimal처럼 쉽게 사용할 수 있겠죠. 하지만 만약 하위 클래스가 상위 클래스의 동작을 제대로 구현하지 않거나 예상하지 못한 예외를 일으킨다면, 개발자는 당황할 수밖에 없습니다.

바로 이런 문제를 방지하고 올바른 상속 설계를 돕는 원칙이 바로 리스코프 치환 원칙(Liskov Substitution Principle, LSP)입니다. 이번 글에서는 LSP의 개념과 중요성을 이해하고, 코드 예시를 통해 어떻게 실무에 적용할 수 있는지 살펴보겠습니다.


리스코프 치환 원칙(LSP)의 의미와 필요성

리스코프 치환 원칙(LSP)은 SOLID 원칙 중 세 번째로, 바바라 리스코프(Barbara Liskov)가 처음 제안했습니다. 로버트 C. 마틴이 정리한 SOLID 원칙에도 포함되어 있으며, 다음과 같은 정의를 갖고 있습니다.

 
 ST의 하위 타입일 때, 프로그램에서 T 타입의 객체를 S 타입의 객체로 교체해도
 프로그램의 동작에 문제가 없어야 한다.
 

쉽게 말하면, 하위 클래스는 항상 상위 클래스를 완벽하게 대체할 수 있어야 한다는 의미입니다. 만약 이를 지키지 않으면 다형성(polymorphism)이 무너지고, 코드는 예측 불가능한 오류를 발생시킬 수 있습니다. 결과적으로 코드가 깨지기 쉽고 유지보수하기 어렵게 됩니다.


LSP를 준수해야 하는 이유

리스코프 치환 원칙을 지키지 않을 경우, 다음과 같은 문제들이 발생할 수 있습니다.

  • 하위 클래스가 상위 클래스의 기능을 제대로 수행하지 못하거나 예외를 던지는 경우
  • 프로그램이 하위 클래스의 존재를 몰라도 제대로 작동해야 하는데, 이 조건이 충족되지 않을 때
  • 상속 구조가 복잡해져서 코드 변경이 어려워지고 버그가 자주 발생할 때

결과적으로 리스코프 치환 원칙을 준수해야 코드의 유지보수성과 안정성을 확보할 수 있습니다.


LSP 적용 전후 비교 예시

구체적인 코드 예시를 통해 LSP 적용 전과 후를 비교해 보겠습니다.

LSP를 위반한 예시

아래 코드는 직사각형과 정사각형을 상속으로 구현한 예시입니다.

lsp
class Rectangle {
  protected width: number
  protected height: number
 
  setWidth(width: number) {
    this.width = width
  }
 
  setHeight(height: number) {
    this.height = height
  }
 
  getArea() {
    return this.width * this.height
  }
}
 
class Square extends Rectangle {
  setWidth(width: number) {
    this.width = width
    this.height = width // 너비와 높이가 같음
  }
 
  setHeight(height: number) {
    this.height = height
    this.width = height // 높이와 너비가 같음
  }
}
 
function calculateArea(shape: Rectangle) {
  shape.setWidth(5)
  shape.setHeight(4)
  console.log(shape.getArea()) // 기대 값: 20
}
 
const rectangle = new Rectangle()
const square = new Square()
 
calculateArea(rectangle) // 20 (정상)
calculateArea(square) // 16 (예상과 다름)

이 코드에서 Square 클래스는 Rectangle의 역할을 제대로 하지 못합니다. 직사각형처럼 동작할 것을 기대했지만, 실제로는 정사각형이라는 특성 때문에 다르게 동작해 LSP를 위반하고 있습니다.


LSP를 적용한 개선된 예시

상속 대신 인터페이스를 활용하여 개선하면 다음과 같이 설계할 수 있습니다.

lsp
interface Shape {
  getArea(): number
}
 
class Rectangle implements Shape {
  constructor(private width: number, private height: number) {}
 
  getArea(): number {
    return this.width * this.height
  }
}
 
class Square implements Shape {
  constructor(private side: number) {}
 
  getArea(): number {
    return this.side * this.side
  }
}
 
function calculateArea(shape: Shape) {
  console.log(shape.getArea())
}
 
const rectangle = new Rectangle(5, 4)
const square = new Square(4)
 
calculateArea(rectangle) // 20
calculateArea(square) // 16

이제 SquareRectangle 모두 Shape 인터페이스를 통해 공통된 방식으로 사용되며, 예측 가능하고 명확한 동작을 수행합니다.


LSP를 적용할 때의 주의사항

리스코프 치환 원칙을 적용할 때는 다음을 주의해야 합니다.

  • 상속이 아니라 인터페이스를 통해 다형성을 확보하는 방식을 우선적으로 고려하세요.
  • 하위 클래스가 상위 클래스의 계약(메서드, 속성, 반환 값 등)을 위반하지 않도록 명확한 기준을 잡으세요.
  • 기능 확장이 필요할 때는 상속보다는 구성(Composition)을 통해 유연성을 확보하세요.

정리

리스코프 치환 원칙(LSP)은 하위 클래스가 상위 클래스의 역할을 완벽히 대체할 수 있어야 한다는 원칙입니다. 이를 지키지 않으면 코드가 예측하기 어렵고 유지보수가 어려워지기 때문에, 상속 구조를 설계할 때 항상 고려해야 하는 중요한 원칙입니다.

이 원칙을 준수하면 코드의 안정성과 유연성이 높아져, 유지보수가 쉽고 예측 가능한 객체 지향 프로그래밍이 가능해집니다. 이를 위해서 상속을 남발하지 말고 인터페이스를 적극적으로 활용하며, 때로는 구성(Composition) 방식으로 접근하는 것이 좋습니다.


공식 문서 및 참고자료