1. 의미
- Liskov subsitution principle.
- 프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.
- 따라서 상호 대체 가능한 구성 요소를 이용해 소프트웨어 시스템을 만들 수 있으려면, 이들 구성요소는 반드시 서로 치환 가능해야 한다는 계약을 반드시 지켜야 한다.
- 다형성을 지원하기 위한 원칙
- 사전에 약속한 기획대로 구현하고, 상속 시 부모에서 구현한 원칙을 따라야 한다가 이 원칙의 핵심
2. 원칙 위반 예제1
-
자식 클래스가 오버라이딩을 잘못하는 경우 발생
-
자식 클래스가 부모 클래스의 메소드 시그니처를 자기 멋대로 변경하는 경우
class Animal { int speed = 100; int go(int distance) { return speed * distance; } } class Eagle extends Animal { String go(int distance, boolean flying) { if (flying) return distance + "만큼 날아서 갔습니다."; else return distance + "만큼 걸어서 갔습니다."; } } public class Main { public static void main(String[] args) { Animal eagle = new Eagle(); eagle.go(10, true); } }- Animal 클래스를 상속하는 Eagle 자식 클래스가 부모 클래스의 go() 메소드를 재사용 한답시고 메소드 타입을 바꾸고 매개변수 갯수도 바꿔버림
- 어느 메소드를 오버로딩을 부모가 아닌 자식 클래스에서 해버렸기 때문에 발생한 LSP 위반 예제
- 부모 클래스의 행동 규약을 어긴 셈, 오류 발생
-
자식 클래스가 부모 클래스의 의도와 다르게 메소드를 오버라이딩 하는 경우
class NaturalType { String type; NaturalType(Animal animal) { // 생성자로 동물 이름이 들어오면, 정규표현식으로 매칭된 동물 타입을 설정한다. if(animal instanceof Cat) { type = "포유류"; } else { // ... } } String print() { return "이 동물의 종류는 " + type + " 입니다."; } } class Animal { NaturalType getType() { NaturalType n = new NaturalType(this); return n; } } class Cat extends Animal { } // --- public class Main { public static void main(String[] args) { Animal cat = new Cat(); **String result = cat.getType().print();** System.out.println(result); // "이 동물의 종류는 포유류 입니다." } }-
Animal 클래스에 확장되는 동물들(Cat, Dog, Lion ...등)을 다형성을 이용하여 업캐스팅으로 인스턴스화 해주고, getType() 메서드를 통해 NautralType 객체 인스턴스를 만들어 NautralType의 print() 메서드를 출력하여 값을 얻는 형태
-
협업하는 다른 개발자가 자기 멋대로 자식 클래스에 부모 메서드인 getType() 의 반환값을 null로 오버라이딩 설정하여 메서드를 사용하지 못하게 설정하고, 대신 getName() 이라는 메서드를 만들어 한번에 출력하도록 설정하게 되면 기존에 동작하던 코드는 예외가 발생하게 됨
class Cat extends Animal { @Override NaturalType getType() { return null; } String getName() { return "이 동물의 종류는 포유류 입니다."; } }
-
-
3. 원칙 위반 예제2

abstract class Animal {
void speak() {}
}
class Cat extends Animal {
void speak() {
System.out.println("냐옹");
}
}
class Dog extends Animal {
void speak() {
System.out.println("멍멍");
}
}
class Fish extends Animal {
void speak() {
try {
throw new Exception("물고기는 말할 수 없음");
} catch (Exception e) {
e.printStackTrace();
}
}
}
public class Main {
public static void main(String[] args) {
List<Animal> list = new ArrayList<>();
list.add(new Cat());
list.add(new Dog());
list.add(new Fish());
for(Animal a : list) {
a.speak();
}
}
}- LSP 원칙에 따르면 speak() 메서드를 실행하면 각 동물 타입에 맞게 울부짖는 결과를 내보내야 되는데, 갑자기 뜬금없이 예외를 던져버리니 개발자 간 상호 신뢰를 잃게 될수도 있음
- 리스코프 치환 원칙은 협업하는 개발자 사이의 신뢰를 위한 원칙이기도 함
4. 수정한 예제

abstract class Animal {
}
interface Speakable {
void speak();
}
class Cat extends Animal implements Speakable {
public void speak() {
System.out.println("냐옹");
}
}
class dog extends Animal implements Speakable {
public void speak() {
System.out.println("멍멍");
}
}
class Fish extends Animal {
}5. LSP 원칙 적용 주의점
- LSP 원칙의 핵심은 상속(Inheritance)
- 객체 지향 프로그래밍에서 상속은 기반 클래스와 서브 클래스 사이에 IS-A 관계가 있을 경우로만 제한되어야 함
- 그 외의 경우에는 합성(composition)을 이용하도록 권고하고 있음
- 다형성을 이용하고 싶다면 extends 대신 인터페이스로 implements하여 인터페이스 타입으로 사용하기를 권장
- 상위 클래스의 기능을 이용하거나 재사용을 하고 싶다면 상속(inheritnace) 보단 합성(composition)으로 구성하기를 권장
Reference
- 로버트 C.마틴, 2019, 클린 아키텍처: 소프트웨어 구조와 설계의 원칙
- https://ko.wikipedia.org/wiki/SOLID*(객체*지향_설계)
- https://inpa.tistory.com/entry/OOP-💠-아주-쉽게-이해하는-LSP-리스코프-치환-원칙