객체지향에서 자주 듣는 SOLID 원칙 중 하나가 LSP(Liskov Substitution Principle, 리스코프 치환 원칙)이다. 말만 들으면 어렵지만, 실제로는 “상속을 제대로 쓰기 위한 최소한의 규칙”에 가깝다.
부모 타입에 자식 타입을 넣어도 아무 문제 없이 동작해야 한다.
즉, 부모가 기대하는 행동을 자식도 반드시 지켜야 한다.
상속은 코드 재사용에 좋지만, 잘못 쓰면 구조가 엉망이 된다.
특히 자식 클래스가 부모 클래스의 약속을 깨버리면, 다음과 같은 문제가 생긴다.
LSP는 이런 상황을 방지하기 위한 기준이다.
고전적인 예제로 많이 나오는 케이스다.
class Rectangle {
public:
virtual void SetWidth(int w) { width = w; }
virtual void SetHeight(int h) { height = h; }
protected:
int width, height;
};
class Square : public Rectangle {
public:
void SetWidth(int w) override {
width = height = w;
}
void SetHeight(int h) override {
width = height = h;
}
};
문제는 부모를 기준으로 코드가 작성되어 있을 때 발생한다.
void ResizeTo(Rectangle& rect) {
rect.SetWidth(10);
rect.SetHeight(20);
// Rectangle이라면 width=10, height=20을 기대하지만,
// Square가 들어오면 width=20, height=20이 되어버린다.
}
정사각형은 직사각형의 규칙(가로와 세로가 독립적)을 지킬 수 없으므로 상속 관계 자체가 잘못된 것이다.
LSP를 지키지 않은 대표적 사례.
예: 부모 함수는 아무 때나 SetWidth 가능하지만,
자식에서 “특정 조건에서만 가능하다”고 바꾼다면 LSP 위반이다.
예: 부모의 Move()는 1m 이동인데
자식이 Move()를 2m 이동하게 만들면 안 된다.
부모는 정상 실행인데
자식은 자꾸 예외를 던지면 부모의 계약을 위반한 것이다.
부모는 int를 받는데
자식은 float만 받는 식으로 바꿔버리면 완전히 어긋난다.
class AWeapon {
public:
virtual void Attack() { /* 기본 공격 */ }
};
class ABow : public AWeapon {
public:
void Attack() override {
// 조건: 무조건 화살이 있어야만 Attack 가능
// 부모는 조건 없이 Attack 가능한데 자식이 제한 조건을 추가함
}
};
부모 기준으로 Attack()은 어떤 상황에서도 실행되리라 기대하지만,
자식이 자기 마음대로 조건을 추가하면 부모를 치환했을 때 코드가 깨진다.
LSP는 “상속을 설계할 때 최소한의 약속을 지키도록 강제하는 원칙”이다.
정리하면 다음과 같다.
이 기준만 지켜도 클래스 구조가 훨씬 견고해진다.
상속 관계라면 “부모처럼 행동할 수 있는가?”를 반드시 확인해야 한다.
SOLID 중 LSP는 실제로 상속 설계할 때 가장 자주 깨지는 원칙이기도 하다.