Liskov substitution principle : 리스코프 치환 원칙
리스코프 형님이 만드신 원칙으로, ‘프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.’ 라고 정의되어 있다.
이게 말이 참 어려운데.. 다른 정의도 살펴보자.
‘자식클래스는 부모 클래스로써의 역할을 완벽히 할 수 있어야 한다’
일단 두 개의 정의를 살펴보니 상속에 관한 얘기인 것 같고.. 아주 쉽게 정리하면 리스코프 원칙 따라서 상속 야무지게 해라! 라는 뜻으로 봐도 될 것 같다^^
나는 처음 봤을 때 상속을 잘 하라는건 알겠는데 의문이 드는 말이 하나 있었다.
‘부모 클래스를 자식 클래스로 대체해도 문제 없어야 한다’
이게 도대체 무슨말이지..?
많은 예시를 보면서 오래 고민해보았는데 이렇게 정리할 수 있을 것 같다.
부모가 정해준 속성과 메서드를 사용하지 않거나 비워둔다면 LSP 위반!
부모가 정해준 속성과 메서드의 의미를 변형하여 사용하면 LSP 위반!
비유하자면 부모님이 물려준 재산 내 맘대로 사용하지 말고, 부모님 유서대로 사용하자! (맞나 이거..?)
그렇다면 LSP를 지키면 어떤 장점을 가져갈 수 있을까?
내 생각에는 LSP를 위반하지 않은 자식 인스턴스를 유연하게 교체하며 사용할 수 있는, 한마디로 재사용성이 뛰어난 장점이 있는 것 같다.
간단한 예시 코드를 보면서 이해해보자.
class Bird {
func fly() {
print("Flying")
}
}
class Sparrow: Bird {
override func fly() {
print("Flying like a sparrow")
}
}
class Ostrich: Bird {
override func fly() {
fatalError("Ostriches cannot fly") // 재정의하지 않고 에러 발생
}
}
func makeBirdFly(bird: Bird) {
bird.fly()
}
// 사용 예시
let sparrow = Sparrow()
let ostrich = Ostrich()
makeBirdFly(bird: sparrow) // 출력: "Flying like a sparrow"
makeBirdFly(bird: ostrich) // 런타임 에러: "Ostriches cannot fly"
Bird라는 부모 클래스가 있고 이 클래스를 상속 받는 2개의 자식 클래스가 있다.
부모가 정해준 메서드인 fly()를 참새 클래스(sparrow)에서는 의도에 맞게 구현하고 있지만 타조 클래스에서는 fly()함수를 사용하지 못하게 구현해 놓았다.
타조는 날 수 없기 때문에 원하는 메서드를 구현하려면 아래와 같이 변형하여 코드를 구현하여야 한다.
class Ostrich: Bird {
override func fly() {
print("Running like a ostrich") // fly() 의도에 맞지 않게 변형하여 구현
}
}
// OR
class Ostrich: Bird {
override func fly() {
fatalError("Ostriches cannot fly")
}
func run() { // 부모가 정해주지 않은 새로운 함수 생성
print("Running like a ostrich")
}
}
이렇게 부모가 정해준대로 구현하지 않으면 makeBirdFly(bird: ostrich) 에서 오류가 나거나, 의도하지 않은 동작을 하게 되는 것이다.
아까 보았던 자식은 부모를 대체할 수 있어야 한다는 의미도, 부모인 bird: 파라미터 자리에 LSP를 위반하지 않는 자식을 넣으면 문제 없이 돌아간다는 의미로 봐도 될 것 같다.
어쨌든 정의만 봤을 때는 꽤나 어려웠지만 배우고 나니 생각보다 이해하기 쉬운 원칙인 것 같다.
물론 이 원칙을 지키며 코드를 짜는 것은 매우 어려울 것이다. ㅋㅅㅋ
부모님이 시키는 것만 하자 !
Interface segregation principle : 인터페이스 분리 원칙
먼저 정의를 살펴보면 이렇다.
“특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.”
“클라이언트들이 사용하지 않는 인터페이스를 의존하도록 강요되어선 안된다.”
나는 개인적으로 ISP는 SRP + LSP인 합친 느낌이 난다. (근데 protocol로 곁들여진..)
LSP법칙 처럼 어떤 protocol(inderface)를 채택한 클래스에서 무력화한 기능이 존재한다면 위반한 것이고, 그 얘기는 그 protocol이 1개의 책임만을 가진 것이 아니라고 봐도 돼서 그렇게 생각한 것이다.
간단한 예시를 들어 설명하면 “움직이다”와 “멈추다” 기능을 가지고 있는 프로토콜이 있을 때, 이 프로토콜을 채택한 클래스에서 두 기능 중 한 기능이라도 무력화 한다면 그 것은 ISP를 위반한 것이다.
“움직이다” 기능을 가진 프로토콜과 “멈추다” 기능을 가진 프로토콜로 나누어 해결해야 한다.
물론 무차별적으로 프로토콜을 분리하며 구현하면 코드가 복잡해질 수 있기 때문에 기준을 잘 잡고 프로토콜 하나에 필요한 기능들만 적절히 구현해줘야 한다.
ISP를 위반하지 않으려면 Protocol을 구현할 때 SRP와 LSP를 잘 지켜라!