[객체 지향 설계 5원칙 - SOLID] 3.LSP
Liskov Substitution Principle : 리스코프 치환 원칙
LSP란 서브 타입은 언제나 자신의 기반 타입으로 교체할 수 있어야 한다는 원칙이다.
상속에 대해 설명하면서 객체 지향에서의 상속은 조직도나 계층도가 아닌 분류도가 돼야 한다고 했다.
객체 지향의 상속은 다음의 조건을 만족해야 한다.
- 하위 클래스 is a kind of 상위 클래스 - 하위 분류는 상위 분류의 한 종류이다.
- 구현 클래스 is able to 인터페이스 - 구현 분류는 인터페이스할 수 있어야 한다.
위 두개의 문장대로 구현된 프로그램이라면 이미 리스코프 치환 원칙을 잘 지키고 있다고 할 수 있다.
반대로 상속이 조직도나 계층도 형태로 구축된 경우 LSP을 지켰다 볼 수 없다.
아버지 춘향이 = new 딸()
춘향이란 딸은 아버지 행위를 할 수 있어야 하는데 딸에게 아버지의 어떤 역활을 시킬 수 있을까?
동물 뽀로로 = new 펭귄()
펭귄 한 마리가 태어나 뽀로로라 이름을 짓고 동물의 행위를 하게하는데 전혀 이상함이 없다.
아버지-딸 구조, 조직도/계층도는 리스코프 치환 원칙을 위배하고 있는것이며
동물-펭귄 구조, 분류도는 리스코프 치환 원칙을 만족하는 것이다.
그럼 LSP를 다시 의역하자면
하위 클래스의 인스턴스는 상위형 객체 참조 변수에 대입해 상위 클래스의 인스턴스 역활을 하는데 문제가 없어야 한다.
분류도인 경우 하위에 존재하는 것들은 상위에 있는 것들의 역활을 하는데 전혀 문제가 없다.
고래가 포유류 또는 동물의 역활을 하는 것은 전혀 문제가 되지 않는다.
결국 리스코프 치환 원칙은 객체 지향의 상속이라는 특성을 올바르게 활용하면 자연스럽게 얻는 것이다.
"상위 클래스는 풍성할수록, 인터페이스는 작을수록 좋다"
다음 그림을 토대로 밑의 코드를 보자.
import java.util.Date;
public class Driver {
public static void main(String[] args) {
사람 김학생 = new 학생("김학생", new Date(2000, 01, 01), "20000101-1234567",
"20190001");
사람 이군인 = new 군인("이군인", new Date(1998, 12, 31), "19981231-1234567",
"19-12345678");
System.out.println(김학생.이름);
System.out.println(이군인.이름);
System.out.println(김학생.생일);
System.out.println(이군인.생일);
System.out.println(김학생.주민등록번호);
System.out.println(이군인.주민등록번호);
System.out.println(김학생.학번);
System.out.println(이군인.군번);
System.out.println(((학생) 김학생).학번);
System.out.println(((군인) 이군인).군번);
김학생.먹다();
이군인.먹다();
김학생.자다();
이군인.자다();
김학생.소개하다();
이군인.소개하다();
김학생.공부하다();
이군인.훈련하다();
((학생) 김학생).공부하다();
((군인) 이군인).훈련하다();
}
}
빈약한 클래스일 때랑 풍성한 클래스일 때랑 결과값을 예상해 보자.
빈약한 상위 클래스를 이용한 경우 여기저기 형변환이 발생하면서 상속의 혜택을 제대로 누리지 못함을 알 수 있다.
상위 클래스형의 참조 변수를 이용해야 상속의 가장 큰 혜택을 볼 수 있다.
풍성한 상위 클래스를 이용한 경우 사용 불가능한 경우나 불필요한 형변환이 줄었다.
즉, 상위 클래스가 풍성할 수록 좋은 것은 LSP에 따라 하위 객체는 상위 객체인 척 할 수 있다라는 자료형 다형성의 이점에 따른 이유라 볼 수 있다.
LSP 원칙 적용 주의점
LSP이란, 다형성의 특징을 이용하기 위해 상위 클래스 타입으로 객체를 선언하여 하위 클래스의 인스턴스를 받으면,
업캐스팅된 상태에서 부모의 메서드를 사용해도 동작이 의도대로만 흘러가도록 구성하면 되는 것이다.
그리고 LSP 원칙의 핵심은 상속(Inheritance)이다.
객체 지향 프로그래밍에서 상속은 기반 클래스와 서브 클래스 사이에 Is kind of 관계가 있을 경우로만 제한 되어야 한다.
그 외의 경우에는 합성(composition)을 이용하도록 권고되어 있다.
다형성을 이용하고 싶다면 extends 대신 인터페이스로 implements하여 인터페이스 타입으로 사용하기를 권하며,
상위 클래스의 기능을 이용하거나 재사용을 하고 싶다면 상속 보단 합성(composition)으로 구성하기를 권장한다.
📖Reference
- 도서 '스프링 입문을 위한 자바 객체 지향의 원리와 이해'
- inpa.tistory.com '완벽하게 이해하는 LSP(리스코프 원칙)'