SOLID 원칙 중 하나인 리스코프 치환 원칙(LSP)에 대해 알아볼께요.
🌟 LSP 정복을 위한 학습 계획 🌟
- LSP란 무엇일까?: 리스코프 치환 원칙의 기본 개념을 쉽고 재미있는 비유로 알아봐요.
- LSP는 왜 중요할까?: 이 원칙을 지키면 어떤 점이 좋은지, 왜 필요한지 알아봐요.
- LSP 실제 예시: 간단한 예시를 통해 LSP를 제대로 이해하고, 어떻게 적용하는지 배워봐요.
🐧 LSP란 무엇일까?
리스코프 치환 원칙(Liskov Substitution Principle)을 아주 간단하게 말하면, "자식 클래스는 부모 클래스의 역할을 완벽하게 해낼 수 있어야 한다"는 뜻이에요.
조금 더 쉽게 비유를 들어볼까요?
여기 '새'라는 부모 클래스가 있다고 상상해 보세요. '새'는 fly()라는 기능을 가지고 있어요.
class 새 {
void fly() {
// 하늘을 나는 로직
}
}
이제 '새'를 상속받는 '참새'와 '펭귄'이라는 자식 클래스를 만들어 볼게요.
- 참새: '새'의 자식이죠. 당연히 fly() 기능을 잘 수행할 수 있어요. 문제없죠!
- 펭귄: '새'의 자식이지만... 앗, 펭귄은 하늘을 날 수 없어요! 🐧
만약 펭귄 클래스에서 fly() 기능을 "아무것도 안 하거나" 또는 "오류를 발생"시키도록 만든다면 어떻게 될까요? '새'를 기대하고 fly()를 호출한 프로그램은 펭귄 때문에 예상치 못한 문제를 겪게 될 거예요.

🤔 LSP는 왜 중요할까?
LSP를 지키면 우리 코드가 훨씬 더 튼튼하고 유연해져요. 크게 두 가지 좋은 점이 있답니다.
1. 코드의 신뢰성과 안정성 UP! ⬆️
- LSP를 잘 지키면, 부모 클래스를 사용하는 곳이라면 어디든지 자식 클래스를 믿고 쓸 수 있어요. 아까 '새'의 예시처럼, 어떤 코드가 '새' 객체를 받아서 fly()를 호출한다고 해볼게요. 이때 '참새'가 오든 '독수리'가 오든 모두 fly()를 잘 수행하니까 프로그램은 안정적으로 동작하겠죠? LSP는 이렇게 예측 가능하고 안정적인 프로그램을 만드는 데 도움을 줘요.
2. 새로운 기능을 추가하기 쉬워져요! (확장성) 🧩
- 만약 나중에 '갈매기'라는 새로운 클래스를 추가해야 한다고 상상해 보세요. '갈매기'가 '새'의 자식이고 fly() 기능을 올바르게 구현했다면, 기존에 '새'를 사용하던 코드를 단 한 줄도 고칠 필요 없이 '갈매기'를 바로 사용할 수 있어요. 이것이 바로 LSP가 가져다주는 유연하고 확장 가능한 설계의 힘이에요. 새로운 부품(자식 클래스)을 끼우기 위해 기계 전체(기존 코드)를 뜯어고칠 필요가 없는 거죠.
결론적으로 LSP는 "부품(객체)을 마음 편히 갈아 끼울 수 있는 유연한 설계를 위한 원칙"이라고 생각할 수 있어요.
이처럼 자식 클래스(펭귄)가 부모 클래스(새)의 기능을 올바르게 대체할 수 없을 때, 리스코프 치환 원칙을 위반했다고 말해요. 즉, LSP는 상속 관계에서 신뢰와 일관성을 지키는 약속과도 같아요.
💻 LSP 실제 예시: 정사각형 문제
프로그래밍에서 LSP를 설명할 때 가장 많이 사용하는 '직사각형과 정사각형' 예시가 있어요.
우리가 가진 기본 전제는 이렇습니다:
"정사각형은 특별한 종류의 직사각형이다." 이 생각 때문에 많은 개발자들이 Square 클래스가 Rectangle 클래스를 상속받게 설계하곤 합니다. (논리적으로는 맞는 말 같으니까요!)
문제의 핵심은 "부모 클래스의 기능을 자식 클래스가 그대로 사용할 수 있는가?" 입니다.
자, 여기 한 코드가 있습니다. 이 코드는 "직사각형"만 다룰 줄 압니다. 이 코드의 중요한 전제는 "직사각형은 너비와 높이를 마음대로 조절할 수 있다"는 거예요. 이 코드는 마치 직사각형을 이리저리 늘이고 줄이는 '도형 조작 기계'와 같습니다.

// 부모 클래스: 직사각형
class Rectangle {
protected int width;
protected int height;
public void setWidth(int width) { this.width = width; }
public void setHeight(int height) { this.height = height; }
public int getArea() { return this.width * this.height; }
}
// 자식 클래스: 정사각형
class Square extends Rectangle {
@Override
public void setWidth(int width) {
this.width = width;
this.height = width; // 정사각형은 너비와 높이가 같아야 하므로!
}
@Override
public void setHeight(int height) {
this.width = height;
this.height = height; // 정사각형은 너비와 높이가 같아야 하므로!
}
}
여기까진 문제가 없어 보여요. 하지만 Rectangle을 사용하는 어떤 코드가 있다고 해볼게요.
public void testArea(Rectangle r) {
r.setWidth(5);
r.setHeight(4);
// 직사각형이라면 넓이는 5 * 4 = 20이 나와야 정상!
assert r.getArea() == 20;
}
이 testArea 함수에 new Rectangle()을 넣으면 당연히 테스트를 통과해요.

위 이미지에서 보는 것처럼, Rectangle Manipulator는 setWidth(5) 다음에 setHeight(4)를 호출하면, 너비 5, 높이 4의 직사각형이 될 것이고, 면적은 20이 될 것이라고 '기대'합니다.
만약 이 기계에 '정사각형'을 넣으면? 😱
이제 문제가 발생합니다. 이 Rectangle Manipulator에게 Square 객체를 넣어볼게요.
Square는 Rectangle을 상속받았기 때문에, 기계는 Square를 Rectangle이라고 생각하고 똑같이 다룹니다.
- setWidth(5) 호출: Square는 너비와 높이를 모두 5로 만듭니다. (정사각형이니까요)
- setHeight(4) 호출: Square는 너비와 높이를 모두 4로 만듭니다. (정사각형이니까요)
- 결과: 기계는 면적이 20이 될 것이라고 기대했지만, 실제 면적은 4 * 4 = 16이 되어버렸습니다.
- 결국 16 == 20은 거짓이므로, 테스트는 실패하게 됩니다! 💥

이것이 바로 LSP 위반입니다. testArea()는 Rectangle 객체를 다루는 것처럼 Square 객체를 다루었지만, Square의 예상치 못한 행동 변화(오버라이딩된 setHeight 메서드) 때문에 프로그램의 기대가 깨진 것입니다.
핵심 요약:
- LSP를 지키면: 부모(Rectangle)가 들어갈 자리에 자식(Square)이 들어가도 프로그램은 기대한 대로 잘 작동해야 합니다.
- LSP를 위반하면: 자식이 부모의 기능을 다른 방식으로 동작하게 해서, 부모를 기대하고 만든 코드가 오류를 일으키게 됩니다.
따라서 정사각형과 직사각형은 상속 관계가 아닌, 독립적인 관계를 가지거나(예: 둘 다 Shape라는 더 추상적인 부모를 상속받거나), Rectangle 클래스에 setHeight, setWidth처럼 너비/높이를 독립적으로 설정하는 메서드가 없어야 합니다.
퀴즈!
이 문제를 해결하고 LSP를 지키려면 어떻게 해야 할까요?
- Square가 Rectangle을 상속받지 않도록 설계를 바꾼다.
- testArea 함수가 Square일 경우를 대비해 특별한 코드를 추가한다.
- Rectangle 클래스에 isSquare() 같은 확인용 메서드를 추가한다.
정답 : 1
왜 1번이 정답일까요?
'정사각형은 직사각형의 일종'이라는 일상적인 개념 때문에 상속 관계를 생각하기 쉽지만, 프로그래밍에서는 행동(Behavior)의 관점에서 봐야 합니다.
- 직사각형은 너비와 높이를 독립적으로 변경할 수 있습니다.
- 정사각형은 너비와 높이를 항상 같게 유지해야 합니다.
정사각형이 직사각형을 상속받으면, 직사각형의 setWidth()나 setHeight() 메서드가 정사각형에서는 다르게 작동하거나 (위 예시처럼 한쪽을 바꾸면 다른 쪽도 바뀌는), 아예 동작하지 않아야 하는 경우가 생깁니다. 이렇게 부모의 메서드를 자식에서 다르게 동작하게 하거나 예외를 던지면, 부모를 기대하고 만든 코드가 자식을 만났을 때 예상치 못한 문제를 일으킬 수 있습니다. 이것이 바로 LSP 위반이죠.
따라서, 직사각형과 정사각형은 서로 다른 독립적인 클래스이거나, 둘 다 만족시킬 수 있는 더 추상적인 상위 개념(예: Shape)을 두는 것이 LSP를 지키는 방법입니다.
'소프트웨어공학' 카테고리의 다른 글
| [SOLID] ISP 원칙 (Interface Segregation Principle) (1) | 2025.10.07 |
|---|---|
| [SOLID] SRP 원칙 (Single Responsibility Principle) (0) | 2025.10.05 |
| [SOLID] DIP (Dependencies Inversion Principles) (0) | 2025.10.03 |
| [SOLID] OCP (Open-Closed Principle) (0) | 2025.10.03 |
| [디자인패턴] 상태 패턴 (State Pattern) (2) | 2025.08.29 |