SOLID > LSP (리스코프 치환 원칙)
in etc.
본 글은 로버트 C.마틴 저자(송준이 엮음)의 [클린 아키텍처: 소프트웨어 구조와 설계의 원칙] 도서를 참고하였습니다.
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-리스코프-치환-원칙