리스코프 치환 원칙(Liskov Substitution Principle, LSP)
요약
리스코프 치환 원칙(LSP)은 모든 서브타입 객체가 기반 타입 객체를 대체해도 프로그램의 올바른 동작이 유지되어야 한다는 원칙입니다. 이 원칙은 객체 지향 설계에서 다형성과 확장성을 보장하기 위한 핵심 가이드라인으로, 유지보수성 향상과 코드 안정성 확보에 기여합니다. LSP를 준수하면 새로운 기능 추가 시 기존 코드를 수정하지 않고도 시스템을 확장할 수 있어 소프트웨어 변경 리스크를 최소화할 수 있습니다. 또한 LSP는 SRP, OCP 등 다른 SOLID 원칙과 상호 보완적으로 작용하여 견고한 설계 기반을 제공합니다.
1. LSP란 무엇인가?
1.1 정의와 배경
리스코프 치환 원칙은 Barbara Liskov가 1987년 학회 키노트에서 제시한 데이터 추상화와 계층 구조에 대한 아이디어에서 유래했습니다. LSP의 정확한 정의는 “만약 S가 T의 서브타입이라면, 프로그램의 타당성을 해치지 않고 T 타입의 객체를 S 타입의 객체로 대체할 수 있어야 한다”입니다.
1.2 핵심 개념
하위 타입은 상위 타입이 수행하던 역할을 동일하게 수행해야 합니다. 즉, 서브클래스는 슈퍼클래스의 계약(메서드 시그니처와 동작)을 위반하거나 강화해서는 안 됩니다.
2. LSP가 중요한 이유
첫째, LSP를 지키면 다형성 코드를 활용할 때 예측 가능한 동작을 보장할 수 있어 시스템 안정성이 높아집니다.
둘째, 기능 확장 시 기존 코드를 변경하지 않고 새로운 서브타입을 추가하면 돼 CI/CD 파이프라인 효율이 개선됩니다.
셋째, 팀 협업 환경에서 서로 다른 모듈 간 의존이 최소화되어 병렬 개발이 원활해집니다.
3. LSP 위반 징후
- 다형성을 적용했음에도 불구하고 조건문이 지나치게 많아질 때 OCP뿐 아니라 LSP도 위반했을 가능성이 높습니다.
- 예외 처리가 서브타입마다 다르게 필요해 기반 타입의 메서드 호출이 불안정해진다면 LSP 위반 징후입니다.
- 또한 서브클래스의 전제 조건(preconditions)이 강화되거나 사후 조건(postconditions)이 약화된다면 계약 기반 설계 관점에서 문제가 발생합니다.
4. Java 예제: 위반 → 준수
4.1 위반 예제
다음 코드는 사각형(Rectangle) 클래스를 확장한 정사각형(Square) 클래스가 LSP를 위반하는 상황입니다.
class Rectangle {
protected int width, height;
public void setWidth(int w) { width = w; }
public void setHeight(int h) { height = h; }
public int getArea() { return width * height; }
}
class Square extends Rectangle {
@Override
public void setWidth(int w) {
this.width = w;
this.height = w;
}
@Override
public void setHeight(int h) {
this.width = h;
this.height = h;
}
}
위 코드를 사용할 때 “너비만 변경했을 뿐인데 높이도 함께 변경”되는 부작용이 발생해 LSP가 위배됩니다.
4.2 준수 예제
해결책으로는 공통 인터페이스를 추출하고, 사각형과 정사각형에 맞게 별도 구현체를 제공하는 방법을 적용할 수 있습니다.
interface Shape {
int getArea();
}
class Rectangle implements Shape {
private int width, height;
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
@Override
public int getArea() {
return width * height;
}
}
class Square implements Shape {
private int side;
public Square(int side) {
this.side = side;
}
@Override
public int getArea() {
return side * side;
}
}
5. 실무 적용 체크리스트
- 서브타입이 상위 타입의 계약을 위반하지 않는지 검토합니다.
- 조건문 대신 다형성 기반 구조로 변경할 수 있는지 검토합니다.
- 인터페이스에서 파라미터 전제 조건이나 반환값 사후 조건이 변경되지 않았는지 확인합니다.
- 단위 테스트에서 슈퍼클래스 테스트를 서브클래스 인스턴스로 수행해보는 테스트 케이스를 작성합니다.
6. 흔한 오해와 주의점
오해 실제
서브클래스가 부모 클래스의 모든 메서드를 반드시 오버라이드해야 한다. | 오버라이드는 선택 사항이며, 필요한 기능만 제공하면 됩니다. |
LSP를 지키려면 상속 대신 무조건 인터페이스와 구성을 사용해야 한다. | 상속도 올바른 계약을 지키면 사용할 수 있으나, 계약 위반 우려가 크면 인터페이스 기반 구성이 더 안전합니다. |