[OOP] 리스코프 치환 원칙(LSP: Liskov Substitution Principle) 개념 및 예제
객체 지향 설계 원칙(SOLID)
객체 지향 언어의 등장 이후 수많은 시행착오와 베스트 프랙티스 속에서 객체 지향 설계 5가지 원칙이 등장했는데, 바로 SOLID다. SOLID는 로버트 C. 마틴(Robert C. Martin)이 2000년대 초반 객체 지향 프로그래밍 및 설계의 다섯 가지 기본 원칙으로 제시한 것을 마이클 페더스(Michael Feathers)가 두문자어로 소개한 것이다.
SOLID는 다음 5가지 원칙의 앞 글자를 따서 부르는 이름이다.
- SRP(Single Responsiblity Principle) : 단일 책임 원칙
- OCP(Open Closed Principle) : 개방 폐쇄 원칙
- LSP(Liskov Substitution Principle) : 리스코프 치환 원칙
- ISP(Interface Segregation Principle) : 인터페이스 분리 원칙
- DIP(Dependency Inversion Principle) : 의존 역전 원칙
이 원칙들은 응집도는 높이고(High Cohesion), 결합도는 낮추라(Loose Coupling)는 고전 원칙을 객체지향의 관점에서 재정립한 것이다.
리스코프 치환 원칙(LSP: Liskov Substitution Principle)
“서브 타입(Sub type)은 언제나 자신의 기반 타입(Base type)으로 교체할 수 있어야 한다.”
- 로버트 C. 마틴 -
객체 지향에서의 상속은 조직도나 계층도가 아닌 분류도가 돼야 한다. 객체 지향의 상속은 다음 조건을 만족해야 한다.
- 하위 클래스 is a kind of 상위 클래스 - 하위분류는 상위 분류의 한 종류다.
- 구현 클래스 is able to 인터페이스 - 구현 분류는 인터페이스할 수 있어야 한다.
위 두 조건을 만족하는 경우 리스코프 치환 원칙을 잘 지키고 있다고 할 수 있다.
‘인터페이스할 수 있어야 한다’는 의미는 다음과 같다.
- AutoCloseable - 자동으로 닫힐 수 있어야 한다.
- Appendable - 덧붙일 수 있어야 한다.
- Cloneable - 복제할 수 있어야 한다.
- Runnable - 실행할 수 있어야 한다.
리스코프 치환 원칙 예시
객체 지향에서의 상속은 조직도나 계층도가 아닌 분류도가 되어야 한다.
리스코프 치환 원칙을 위반하는 경우
객체 지향의 잘못된 상속에는 대표적으로 계층도가 있다.
아버지라는 상위 클래스(기반 타입)로 아들과 딸이라는 하위 클래스(서브 타입)가 있다고 가정하자. 이는 전형적인 계층도 형태로 객체 지향에서는 다음과 같이 인스턴스를 할당할 수 있다.
Father 아들 = new Son();
Father 딸 = new Daughter();
- 상위 클래스의 객체 참조 변수에 하위 클래스의 인스턴스를 할당한다.
- 이는 아들과 딸이 아버지의 역할을 하고 있다는 의미기도 하다.
- 아들과 딸은 Father 타입의 객체이기 때문에 Father 객체가 가진 행위(메서드)를 할 수 있어야 하게 된다.
리스코프 치환 원칙을 만족하는 경우
다음은 분류도 형태인 경우다.
동물 클래스와 이를 상속하는 펭귄 클래스가 있다고 가정한다.
Animal 뽀로로 = new Penguin()
- 논리적인 흠이 없다. 펭귄 한 마리가 태어나 뽀로로라는 이름을 갖고 Animal 타입으로 동물의 행위를 할 수 있다.
위 내용을 정리하면 다음과 같다.
- 아버지와 아들 딸의 관계는 리스코프 치환 원칙을 위배하고 있는 것이다.
- 동물과 펭귄 구조는 리스코프 치환 원칙을 만족하고 있는 것이다.
하위 클래스의 인스턴스는 상위 타입 객체 참조 변수에 대입해 상위 클래스의 인스턴스 역할을 하는 데 문제가 없어야 한다.
자바의 컬렉션 프레임워크
리스코프 치환 원칙을 만족하는 좋은 예시로 자바의 컬렉션 프레임워크가 있다.
- ArrayList, LinkedList, Vector는 List 인터페이스의 구현체로 모두 List인 기반 타입으로 업캐스팅하여 사용할 수 있다.
List<Integer> list1 = new ArrayList<Integer>();
List<Integer> list2 = new LinkedList<Integer>();
List<Integer> list3 = new Vector<Integer>();
- List 타입으로 생성된 ArrayList, LinkedList 등은 List 인터페이스의 add(), remove() 메서드 등을 공통적으로 사용할 수 있다.
- 이러한 관계는 Set, Queue에서도 똑같이 적용된다.
다음은 컬렉션 프레임워크를 사용한 간단한 예시다.
public class LSPExample {
public static void main(String[] args) {
Set<Integer> set = new HashSet<>();
set.add(1);
set.add(2);
System.out.println("set = " + set);
Collection<Integer> collection = set;
collection.add(3);
System.out.println("collection = " + collection);
collection = new LinkedList<>();
collection.add(4);
System.out.println("collection = " + collection);
}
}
set = [1, 2]
collection = [1, 2, 3]
collection = [4]
- Set 타입의 HashSet 인스턴스(서브 타입)는 Collection 타입(기반 타입)으로 변환이 가능하다.
- HashSet으로 초기화된 Collection 타입은 LinkedList와 같은 전혀 다른 구현체로도 바꿀 수 있다.
- 리스코프 치환 원칙은 다형성을 지원하기 위한 원칙이기도 하다.
정리
- 서브 타입의 클래스는 언제나 기반 타입으로 교체할 수 있어야 한다.
- 계층도/조직도가 아닌 분류도로 상속 관계를 정의해야 한다.
참고 서적
https://link.coupang.com/a/baQPj6
"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다."