LSF(liskov Substitution Principal) 리스코프 치환 원칙이란?

리스코프 치환 원칙은 자식 클래스는 언제기 부모 클래스를 대체할 수 있어야 한다는 원칙입니다.

즉 부모 클래스를 사용하는 코드가 자식 클래스로 교체되어도 정상적으로 동작해야 한다는 것입니다.

다형성 원리를 얘기하는 것이라고 할 수 있습니다.

가장 잘 원칙을 적용한 예제가 바로 Java의 Collection Framework 입니다!

Collection의 추상 메서드를 하위 자료형 List, Set과 같은 클래스에서 상속(implement)받아 인터페이스 구현 규약을 잘 지키도록 설계되었습니다.

잘못된 짧은 예시를 보겠습니다.

1. 잘못된 메소드 구현

open class Animal {
  private val speed: Int = 100
  
  fun go(distance: Int): Int {
    retrun speed + distance
  }
}

class Eagle: Animal() {
  
  fun go(distance: Int, flying: Boolean): String {
    if(flying) {
      return "${distance}m만큼 날았습니다."
    }else {
      return "${distance}m만큼 걸었습니다."
    }
  }
}

위의 예시를 보면 Animal 클래스의 메소드를 상속받은 Eagle 클래스는 잘못된 메소드의 재사용으로 해당 부모클래스의 행동 규약을 어기게 되었습니다. 이렇게 규약을 어기면 다형성 코드 동작 자체가 되지 않습니다. 이럴때는 Eagle 클래스의 메소드를 새로 만들어 사용하는 것이 옳은 방법일 수 있습니다.

2. 부모 클래스의 의도와 다르게 메소드를 오버라이딩


class NaturalType(animal: Animal) {
  private var type: String = ""
  init {
    type = if (animal is Cat) {
      "포유류"
    }else {
      "알 수 없는 종류"
    }
  }
  
  fun print(): String {
    return "이 동물의 종류는 $type 입니다."
  }
}

open class Animal {
  open fun getType(): NaturalType {
    return NaturalType(this)
  }
}

class Cat: Animal()

fun main() {
  val cat = Cat()
  println(cat.getType().print())
}

위의 코드는 Animal 클래스에 확장되는 동물들을 인스턴스화 해서 getType() 메서드를 통해서 NaturalType 객체 인스턴스를 생성해서 출력하여 값을 얻는 형태인데

만약에 이 코드를 이용하는 다른 개발자가 Dog라는 메서드를 생성하여 한번에 출력하도록 설정을 변경한다면

class Dog: Animal() {
  override fun getType(): NaturalType {
    return null
  }
  
  fun getName(): String {
    return "이 동물은 포유류입니다.
  }
}

위 처럼 구성했다고 Kotlin에서는 NullSafety가 발동하여 Animal의 NaturalType이 Null이 될 수 없다고 나오겠지만 이를 임의로 Nullable로 수정한다면 오류가 발생할 가능성이 있습니다.

따라서 사전에 약속한 기획대로 구현하고 상속 시, 부모에서 구현한 원칙을 따라야 한다는 것이 LSP의 핵심이라고 할 수 있겠습니다.

해당 공부는 널널한 개발자님과 인파님의 블로그를 참고하였습니다.