[객체지향] SOLID: 리스코프 치환 원칙(Liskov Substitution Principle, LSP)
Reference
스프링 입문을 위한 자바 객체 지향의 원리와 이해, 위키북스, 김종민 지음
개발자가 반드시 정복해야 할 객체 지향과 디자인 패턴, 인투북스, 최범균 지음
📌 LSP: 다형성에 관한 원칙
자료형 S가 자료형 T의 서브타입이라면 필요한 프로그램의 속성의 변경 없이 자료형 T의 객체를 S의 객체로 치환할 수 있어야 한다.
- 상위 타입의 객체를 하위 타입의 객체로 치환해도 상위 타입을 사용하는 프로그램은 정상적으로 동작해야 한다. 즉, 서브 타입은 언제나 자신의 기반(상위) 타입으로 교체 할 수 있어야 한다.
- 하위 클래스의 인스턴스는 상위형 객체 참조 변수에 대입해 상위 클래스의 인스턴스 역할을 하는 데 문제가 없어야 한다.
- 프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.
상위 타입인 SuperClass, 하위 타입인 SubClass가 존재할 때, Main 에서 someMethod 를 호출하는 메서드가 존재한다고 가정한다. 호출 메서드는 상위 타입을 파라미터로 사용하고 있는데 하위 타입의 객체를 전달해도 메서드가 정상적으로 호출되어야 한다는 것이 리스코프 치환 원칙이다.
class SuperClass {
public void someMethod() {
System.out.println("Some Method...");
}
}
class SubClass extends SuperClass {
}
public class Main {
public static void callSomeMethod(SuperClass sc) {
sc.someMethod();
}
public static void main(String[] args) {
callSomeMethod(new SuperClass());
callSomeMethod(new SubClass()); // 리스코프 치환 원칙
}
}
- LSP는 다형성에 관한 원칙이다. 만약 LSP 위반이 발생하면 추상화와 다형성을 이용한 OCP도 위반되는 연쇄 효과를 일으키게 된다.
상속의 원칙: 분류도
객체 지향에서의 상속은 조직도나 계층도가 아닌 분류도가 돼야 한다
- 서브 클래스 is a kind of 상위 클래스: 하위 분류는 상위 분류의 한 종류다
- 구현 클래스 is able to 인터페이스: 구현 분류는 인터페이스할 수 있어야 한다
- AutoCloseable: 자동으로 닫힐 수 있어야 한다
- Appendable: 덧붙일 수 있어야 한다
- Cloneable: 복제할 수 있어야 한다
- Runnable: 실행할 수 있어야 한다

- 공통되는 부분, 변경되는 부분을 추상화 시켜야 한다
- 객체 지향의 상속이라는 특성을 올바르게 이해하고 활용한다면 LSP는 자연스럽게 지켜진다.
계층도(조직도) 형태 상속의 문제점
- 상위 클래스의 객체 참조 변수에 하위 클래스의 인스턴스를 할당하게 되면 기존의 기능(계약)을 완벽히 수행할 수 없다. 즉, 서브 타입이 상위 타입을 완벽하게 대체할 수 없다.
// 전형적인 계층도 형태의 상속
class Mother {
}
class Son extends Mother {
}
Mother 석봉이 = new Son();
석봉이는 Mother 객체 참조 변수이기에 Mother의 행위(메서드)를 모두 할 수 있어야 하는데 아들이 엄마의 역할을 한다는 것은 매우 이상하다.

- 위와 같은 전형적인 계층도(조직도) 형태의 상속은 LSP 위반의 정석이다
📌 LSP를 위반했을 때의 문제
1. 올바르지 못한 상속과 구현
class Rectangle {
private int width;
private int height;
public int getWidth() {
return width;
}
public void setWidth(int width) {
this.width = width;
}
public int getHeight() {
return height;
}
public void setHeight(int height) {
this.height = height;
}
public int getArea() {
return width * height;
}
}
class Square extends Rectangle {
@Override
public void setWidth(int width) {
super.setWidth(width);
super.setHeight(width);
}
@Override
public void setHeight(int height) {
super.setHeight(height);
super.setWidth(height);
}
}
public class Main {
static void increaseHeight(Rectangle rectangle) {
if (rectangle instanceof Square) { // 리스코프 치환 원칙 위배, 메서드가 Rectangle 의 확장에 닫혀 있다.
throw new IllegalArgumentException();
}
if (rectangle.getHeight() <= rectangle.getWidth()) {
rectangle.setHeight(rectangle.getWidth() + 10);
}
}
public static void main(String[] args) {
Rectangle rectangle = new Rectangle();
Rectangle square = new Square();
rectangle.setWidth(10);
rectangle.setHeight(5);
square.setWidth(10);
square.setHeight(5);
System.out.println("Rectangle's area: " + rectangle.getArea()); // 50
System.out.println("Square's area: " + square.getArea()); // 25
}
}
- 동작에 있어
Rectangle클래스와Square는 서로 일관되게 행동하지 않는다.Square높이와 너비 설정을 따로 있을 필요가 없는 것이 좋고Rectangle을 상속받는 것이 오히려 혼란스럽게 만든다. 둘은 개념적으로 상속관계에 있는 것처럼 보이지만 실제 구현에서는 상속 관계가 아님을 보여준다. - 정사각형과 직사각형은 사각형이라는 공통점이 있다. 이를 추상화 시켜 두 클래스를 형제 관계로 만든다면 LSP 위반을 해결할 수 있다.
interface Shape {
public int getArea();
}
class Rectangle implements Shape {
private int width;
private int height;
public int getWidth() {
return width;
}
public void setWidth(int width) {
this.width = width;
}
public int getHeight() {
return height;
}
public void setHeight(int height) {
this.height = height;
}
@Override
public int getArea() {
return this.width * this.height;
}
}
class Square implements Shape {
private int width;
private int height;
public void setSize(int length) {
this.width = length;
this.height = length;
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
@Override
public int getArea() {
return this.width * this.height;
}
}
public class Main {
public static void main(String[] args) {
Rectangle rectangle = new Rectangle();
Square square = new Square();
rectangle.setWidth(10);
rectangle.setHeight(5);
square.setSize(5);
System.out.println("Rectangle's area: " + rectangle.getArea()); // 50
System.out.println("Square's area: " + square.getArea()); // 25
}
}
2. 상위 타입에서 지정한 리턴 값의 범위에 해당되지 않는 값을 리턴
입력 스트림으로부터 데이터를 읽어 와 출력 스트림에 복사해 주는 복사 기능 CopyUtil을 구현한 예제이다
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
class CopyUtil {
static void copy(InputStream in, OutputStream out) throws IOException {
byte[] data = new byte[512];
int len = -1;
while ((len = in.read(data)) != -1) { // 0이 리턴되어 무한루프가 된다.
out.write(data, 0, len);
}
}
public static void main(String[] args) throws IOException {
String filePath = "test.txt";
copy(new MyInputStream(), new FileOutputStream(filePath));
}
}
class MyInputStream extends InputStream {
@Override
public int read() throws IOException {
return 0; // -1이 필요하지만 0을 리턴
}
}
MyInputStream이 상위 타입인InputStream을 올바르게 대체하지 않아 문제가 발생한다- 상위에서는
-1을 리턴하지만 서브타입에서0을 리턴하고 있어 서브타입이 상위타입을 완벽하게 대체하지 못하고 있다
- 상위에서는
📌 LSP: 계약과 확장
1. 계약을 위반하지 않는다
하위형에서 상위형의 불변 조건은 반드시 유지돼야 한다.
LSP는 기능의 명세(계약)에 대한 내용이다. 기존의 계약을 위반 하는 사례는 다음과 같다.
- 명시된 명세에서 벗어난 값을 리턴
- 명시된 명세에서 벗어난 예외를 발생
- 명시된 명세에서 벗어난 기능을 수행
기존의 계약에 벗어난 행동을 하게 되면 이 계약을 바탕으로 작성된 코드는 비동상적으로 동작할 수 있기 때문에 항상 하위 타입은 상위 타입에서 정의한 명세를 벗어나지 않는 범위에서 구현해야 한다. 만약 상위 타입에서 정의한 명세를 벗어난 기능이 꼭 필요하다면 잘못된 상속 관계가 발생했음을 의미하니 상속관계를 다시 정의해야 한다.
- 정사각형 - 직사각형 문제에서 정사각형은 상위 타입 직사각형이 정의한 명세를 어긴 구현을 했다. 직사각형은 높이를 설정할 때 전달받은 값으로 높이를 설정하고 너비 값의 변경은 제공하지 않았다. 하지만 정사각형은 높이를 설정할 때 너비 값의 변경을 진행했다.
2. 확장에 열려 있어야 한다
instanceof타입 확인 연산자는 LSP를 위반하는 전형적인 증상이다.
class Item {
int price;
public int getPrice() {
return price;
}
}
class SpecialItem extends Item {
}
class NewItem extends Item {
}
class Coupon {
int discountRate;
public int calculateDiscountAmount(Item item) {
if (item instanceof SpecialItem || item instanceof NewItem) { // LSP 위반
return 0;
}
return item.getPrice() * discountRate;
}
}
위 코드의 문제는 하위 타입이 상위 타입을 완벽하게 대체하지 못한다는 점이다. Item에 관한 새로운 기능이 추가될 때 마다 Coupon은 수정이 필요하게 되는데 이는 확장에는 열려 있고 수정에는 닫혀 있어야 하는 객체지향의 원칙을 위반하고 있다.
Item에 대한 추상화가 필요하다. 변경되는 부분을 상위 타입에 추가한다.
class Item {
int price;
public int getPrice() {
return price;
}
public boolean canDiscount() {
return true;
}
}
class SpecialItem extends Item {
@Override
public boolean canDiscount() {
return false;
}
}
class NewItem extends Item {
@Override
public boolean canDiscount() {
return false;
}
}
class Coupon {
int discountRate;
public int calculateDiscountAmount(Item item) {
if (!item.canDiscount()) {
return 0;
}
return item.getPrice() * discountRate;
}
}
댓글남기기