LSP (Liskov Subsitution Principle) : 리스코프 치환 원칙
1998년 바바라 리스코프(Babara Liskov)가 올바른 상속 관계의 특징을 정의하기 위해 발표한 것으로,
하위 타입(child)은 언제나 상위 타입(parent)을 대체할 수 있어야 한다는 것을 의미한다.
즉, 상위 타입 객체를 하위 타입 객체로 대체하여도 정상적으로 동작해야 한다는 것이다.
LSP의 핵심은 하위 타입이 상위 타입의 동작 규칙(계약)을 따라야 한다는 것이다.
규칙을 따르지 않다는 것은 자식 클래스(하위 타입)가 오버라이딩을 할 때, 잘못되게 재정의하면 리스코프 원칙을 위반할 수 있다는 것이다.
예를 들어 자식 클래스가 부모 클래스 메소드 시그니처를 멋대로 변경하거나, 자식 클래스가 부모 클래스의 의도와 다르게 메서드를 오버라이딩 하는 경우를 말한다.
1. 하위 타입의 잘못된 메서드 오버로딩
Store 클래스를 상속하는 Cafe 자식 클래스가 있다고 가정하자.
open class Store {
fun openAlarm(isOpened : Boolean) : String {
if(isOpened){
return "매장이 오픈되었습니다."
} else {
return "아직 매장이 오픈하지 않았습니다."
}
}
}
class Cafe : Store() {
fun openAlarm(isOpened : Boolean, storeName : String) : String {
if(isOpened)
return "${storeName} 매장은 오픈되었습니다"
else
return "${storeName} 매장은 아직 오픈하지 않았습니다."
}
}
fun main {
val cafe : Store = Cafe()
cafe.openAlarm(true, "스타벅스")
}
스타벅스가 다음과 같이 Store의 openAlarm() 메서드의 매개변수를 멋대로 변경하면 LSP 원칙을 위반하게 된 것이다.
메서드 오버로딩을 부모 클래스가 아닌 자식 클래스 진행했기 때문에 부모 클래스의 행동 규약을 위반한 것이다.
2. 상위 타입 의도와 다른 메서드 오버라이딩
이번엔 Store 클래스를 상속하는 BookStore이라는 클래스가 있고, 무슨 Store인지 알려주는 StoreType 클래스가 있다고 가정하자.
open class StoreType(store : Store) {
var storeType : String = ""
init {
if(store is BookStore) {
storeType = "서점"
else {
...
}
}
fun print() : String {
return "이 매장은 $storeType 입니다."
}
}
open class Store {
open fun getType() : StoreType {
val type = StoreType(this)
return type
}
}
class BookStore : Store ()
다음과 같이 실행하면 '이 매장은 서점 입니다.' 라고 잘 뜰 것이다.
fun main() {
val bookStore: Store = BookStore()
val result = bookStore.getType().print()
println(result)
}
그런데 자식 클래스인 BookStore에서 부모 클래스 메서드인 getType()의 반환값을 type이 아니라 null로 오버라이딩 설정하여
메서드를 사용하지 못하게 설정하고 getName() 이라는 메서드를 만들어 출력하도록 설정했다고 해보자.
class BookStore : Store() {
override fun getType(): StoreType? {
return null
}
fun getName(): String {
return "이 매장은 서점 입니다."
}
}
다음과 같이 재정의 후 메인 코드를 실행하면 NullpointExeption 예외가 발생하게 된다
자식 클래스로 부모 클래스 내용을 상속하는데, 기존 코드에서 보장하던 조건을 수정하거나 적용시키지 않아서, 기존 부모 클래스를 사용하는 코드에서 예상치 못한 오류를 발생시킨 것이다.
따라서 사전에 약속한대로 구현하고, 상속 시 상위 타입에서 구현한 원칙을 따라야 한다는 것이 리스코프 치환 원칙의 핵심이라고 할 수 있다.
3. 잘못된 상속 관계 구성으로 인한 메서드 정의
예를 들어 Store 클래스에 매장의 영업 종료 시간을 추상 메서드 close()를 통해 구현하도록 규칙을 지정했다고 하자.
무인 아이스크림 가게를 추가해야 하는데, 해당 점포는 24시간 영업을 해서 구현할 수 없는 close() 메서드를 구현해야 하는 상황이 발생했다.
따라서 다음과 같이 close()가 동작하지 않고 예외를 던지도록 만들었다.
class IcecreamStore : Store() {
fun close() {
try {
throw Exception("무인 아이스크림 매장은 24시간 영업입니다.")
} catch (e: Exception) {
e.printStackTrace()
}
}
}
이 상황에서 다른 개발자와 협업을 하게 되는데, 이러한 예외 상황을 전달받지 못하는 상황에서 다음과 같은 코드를 실행시키게 된다면 잘 동작하던 코드가 갑자기 예외를 던지게 된다.
LSP 원칙에 따르면 close()를 실행하면 각 영업 종료 시간이 나와야 하는데, 갑자기 예외를 던져버려 개발자 간의 신뢰를 잃게 될 수 있다.
LSP 원칙은 개발자 간 신뢰를 위한 원칙이자 다형성의 특징을 이용하기 위해 하위 타입의 객체가 상위 타입으로 형 변환이 된 상태에서
상위 타입의 메서드를 사용해도 의도대로 동작되도록 구현하는 것이다.
주의해야 할 점은, 객체 지향 프로그래밍에서 상속 관계는 꼭 일반화 관계(IS-A)가 성립해야 한다.
상속 관계가 아닌 클래스들을 상속관계로 설정하면, LSP 위반이다.
IS-A 관계가 아닐 때 다형성을 이용하고 싶다면 implements로, 상위 타입 기능을 이용하거나 재사용하고 싶다면 합성(composition)으로 구성하는 것을 권장한다.
'프로그래밍 > ect' 카테고리의 다른 글
SOLID, 객체 지향 설계 5원칙 : 의존 역전 원칙 (Dependency Inversion Principle) (2) | 2024.06.14 |
---|---|
SOLID, 객체 지향 설계 5원칙 : 인터페이스 분리 원칙 (Interface Segregation Principle) (0) | 2024.06.13 |
SOLID, 객체 지향 설계 5원칙 : 개방 폐쇄 원칙(Open Closed Principle) (0) | 2024.06.08 |
SOLID, 객체 지향 설계 5원칙 : 단일 책임 원칙(Single Responsibility Principle) (0) | 2024.06.07 |
[MySQL] MySQL 날짜 및 시간 관련 함수 (0) | 2024.05.16 |