리스코프 교체 원칙(Liskov Substitution Principle)은 바버라 리스코프(Barbara Liskov)에 의해 제안된 원칙으로, 객체 지향 프로그래밍(OOP)의 설계 원칙 중에 하나이다.
리스코프 교체 원칙은 상속 관계에서의 서브타입(Subtype)의 관계를 정의하고 유지하는 원칙으로, S가 T의 하위타입이라면, T 타입의 객체를 S 타입으로 교체되어도 프로그램의 의미가 변경되지 않아야 한다.
이 원칙은 상속 관계에서 하위 클래스(Subclass)가 상위 클래스(Superclass)의 기능을 대체할 수 있어야 한다는 것을 강조한다. 즉, 부모 클래스로부터 파생된 자식 클래스는 부모 클래스의 행동을 유지하면서도 자신만의 특화된 동작을 추가할 수 있어야 한다.
이를 준수하면 클라이언트 코드에서는 자식 클래스를 사용하더라도 부모 클래스의 인터페이스에 의존할 수 있다. 이는 코드 재사용, 유지보수성, 확장성을 향상시킨다. 하지만 리스코프 교체 원칙을 위반하면 예상치 못한 동작이 발생하거나 시스템의 일관성이 깨질 수 있다.
다음은 리스코프 교체 원칙을 위반하는 예시 Java 소스코드이다.
💡리스코프 교체 원칙 위반 소스코드
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 width * 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과 Square라는 두 개의 클래스가 있다.
Square 클래스는 Rectangle 클래스를 상속받는다.
Square 클래스에서는 setWidth와 setHeight 메서드를 오버라이딩하여 정사각형을 유지한다.
이 경우, Square 클래스는 Rectangle 클래스의 하위 타입으로서 보이지만, 리스코프 교체 원칙을 위반한다. 왜냐하면 Square 클래스의 setWidth와 setHeight 메서드는 부모 클래스의 동작을 정확하게 대체하지 않기 때문이다. Square 객체를 Rectangle 타입으로 교체하면 예상하지 못한 결과가 발생할 수 있다.
예를 들어, Rectangle 변수에 Square 객체를 할당한 다음, setWidth와 setHeight를 사용하여 가로와 세로 길이를 설정하려고 한다고 가정한다.
Rectangle rectangle = new Square();
rectangle.setWidth(5);
rectangle.setHeight(10);
여기서 예상한 결과는 가로와 세로 길이가 각각 5와 10으로 설정되어 직사각형의 넓이가 50이 되는 것이다.
하지만 Square 클래스에서의 오버라이딩된 setWidth와 setHeight 메서드는 가로와 세로 길이를 동일하게 설정하기 때문에, 위의 코드는 실제로는 가로와 세로 길이가 모두 10으로 설정되어 정사각형의 넓이가 100이 되는 결과를 가져온다.
이렇게 Square 객체를 Rectangle으로 다룰 때 기대한 동작과 실제 동작이 다른 경우, 리스코프 교체 원칙이 위반된 것이다.
리스코프 교체 원칙을 준수하기 위해 다음과 같이 소스코드를 수정한다.
💡리스코프 교체 원칙 준수 소스코드
abstract class Shape {
public abstract int getArea(); // 도형의 넓이를 반환하는 추상 메서드
}
class Rectangle extends Shape {
protected int width;
protected int height;
public void setWidth(int width) {
this.width = width;
}
public void setHeight(int height) {
this.height = height;
}
@Override
public int getArea() {
return width * height; // 가로와 세로 길이를 곱하여 넓이 반환
}
}
class Square extends Shape {
protected int side;
public void setSide(int side) {
this.side = side;
}
@Override
public int getArea() {
return side * side; // 한 변의 길이를 제곱하여 넓이 반환 (정사각형의 경우)
}
}
public class Main {
public static void main(String[] args) {
Shape rectangle = new Rectangle(); // Shape 타입으로 Rectangle 객체 생성
rectangle.setWidth(5);
rectangle.setHeight(10);
int rectangleArea = rectangle.getArea(); // Rectangle 객체의 넓이 계산
System.out.println("Rectangle Area: " + rectangleArea);
Shape square = new Square(); // Shape 타입으로 Square 객체 생성
square.setSide(5);
int squareArea = square.getArea(); // Square 객체의 넓이 계산
System.out.println("Square Area: " + squareArea);
}
}
Shape라는 추상 클래스를 도입하여 상속 관계를 수정했다.
각각의 도형은 Shape를 상속받아 구현한다. 그리고 Rectangle과 Square 클래스는 각각 자신에게 맞는 동작을 정의하고, getArea 메서드를 오버라이딩하여 올바른 결과를 반환하도록 수정했다.
이렇게 수정된 코드는 리스코프 교체 원칙을 준수하며, Rectangle과 Square 객체를 Shape 타입으로 교체해도 동작에 일관성이 유지된다.