SOLID > LSP (리스코프 치환 원칙)

본 글은 로버트 C.마틴 저자(송준이 엮음)의 [클린 아키텍처: 소프트웨어 구조와 설계의 원칙] 도서를 참고하였습니다.


1. 의미

  • Liskov subsitution principle.
  • 프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.
  • 따라서 상호 대체 가능한 구성 요소를 이용해 소프트웨어 시스템을 만들 수 있으려면, 이들 구성요소는 반드시 서로 치환 가능해야 한다는 계약을 반드시 지켜야 한다.
  • 다형성을 지원하기 위한 원칙
  • 사전에 약속한 기획대로 구현하고, 상속 시 부모에서 구현한 원칙을 따라야 한다가 이 원칙의 핵심

2. 원칙 위반 예제1

  • 자식 클래스가 오버라이딩을 잘못하는 경우 발생

    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 위반 예제
        • 부모 클래스의 행동 규약을 어긴 셈, 오류 발생
    2. 자식 클래스가 부모 클래스의 의도와 다르게 메소드를 오버라이딩 하는 경우

      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

image

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. 수정한 예제

image

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 관계가 있을 경우로만 제한되어야 함
  • 다형성을 이용하고 싶다면 extends 대신 인터페이스로 implements하여 인터페이스 타입으로 사용하기를 권장
  • 상위 클래스의 기능을 이용하거나 재사용을 하고 싶다면 상속(inheritnace) 보단 합성(composition)으로 구성하기를 권장

Reference