base
개발을 하다 보면 자연스럽게 상속(inheritance)을 자주 사용하게 됩니다. 예를 들어, 일반적인
바로 이런 문제를 방지하고 올바른 상속 설계를 돕는 원칙이 바로 리스코프 치환 원칙(Liskov Substitution Principle, LSP)입니다. 이번 글에서는 LSP의 개념과 중요성을 이해하고, 코드 예시를 통해 어떻게 실무에 적용할 수 있는지 살펴보겠습니다.
리스코프 치환 원칙(LSP)은 SOLID 원칙 중 세 번째로, 바바라 리스코프(Barbara Liskov)가 처음 제안했습니다. 로버트 C. 마틴이 정리한 SOLID 원칙에도 포함되어 있으며, 다음과 같은 정의를 갖고 있습니다.
S는 T의 하위 타입일 때, 프로그램에서 T 타입의 객체를 S 타입의 객체로 교체해도
프로그램의 동작에 문제가 없어야 한다.
쉽게 말하면, 하위 클래스는 항상 상위 클래스를 완벽하게 대체할 수 있어야 한다는 의미입니다. 만약 이를 지키지 않으면 다형성(polymorphism)이 무너지고, 코드는 예측 불가능한 오류를 발생시킬 수 있습니다. 결과적으로 코드가 깨지기 쉽고 유지보수하기 어렵게 됩니다.
리스코프 치환 원칙을 지키지 않을 경우, 다음과 같은 문제들이 발생할 수 있습니다.
결과적으로 리스코프 치환 원칙을 준수해야 코드의 유지보수성과 안정성을 확보할 수 있습니다.
구체적인 코드 예시를 통해 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 (예상과 다름)
이 코드에서
상속 대신 인터페이스를 활용하여 개선하면 다음과 같이 설계할 수 있습니다.
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
이제
리스코프 치환 원칙을 적용할 때는 다음을 주의해야 합니다.
리스코프 치환 원칙(LSP)은 하위 클래스가 상위 클래스의 역할을 완벽히 대체할 수 있어야 한다는 원칙입니다. 이를 지키지 않으면 코드가 예측하기 어렵고 유지보수가 어려워지기 때문에, 상속 구조를 설계할 때 항상 고려해야 하는 중요한 원칙입니다.
이 원칙을 준수하면 코드의 안정성과 유연성이 높아져, 유지보수가 쉽고 예측 가능한 객체 지향 프로그래밍이 가능해집니다. 이를 위해서 상속을 남발하지 말고 인터페이스를 적극적으로 활용하며, 때로는 구성(Composition) 방식으로 접근하는 것이 좋습니다.