-
[SOLID] LSP와 ISP에 대해 알아보자Refactoring 2024. 10. 8. 20:45
LSP
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를 위반하지 않는 자식을 넣으면 문제 없이 돌아간다는 의미로 봐도 될 것 같다.
어쨌든 정의만 봤을 때는 꽤나 어려웠지만 배우고 나니 생각보다 이해하기 쉬운 원칙인 것 같다.
물론 이 원칙을 지키며 코드를 짜는 것은 매우 어려울 것이다. ㅋㅅㅋLSP 한 줄 결론
부모님이 시키는 것만 하자 !
ISP
Interface segregation principle : 인터페이스 분리 원칙
먼저 정의를 살펴보면 이렇다.
“특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.”
“클라이언트들이 사용하지 않는 인터페이스를 의존하도록 강요되어선 안된다.”나는 개인적으로 ISP는 SRP + LSP인 합친 느낌이 난다. (근데 protocol로 곁들여진..)
LSP법칙 처럼 어떤 protocol(inderface)를 채택한 클래스에서 무력화한 기능이 존재한다면 위반한 것이고, 그 얘기는 그 protocol이 1개의 책임만을 가진 것이 아니라고 봐도 돼서 그렇게 생각한 것이다.
간단한 예시를 들어 설명하면 “움직이다”와 “멈추다” 기능을 가지고 있는 프로토콜이 있을 때, 이 프로토콜을 채택한 클래스에서 두 기능 중 한 기능이라도 무력화 한다면 그 것은 ISP를 위반한 것이다.
“움직이다” 기능을 가진 프로토콜과 “멈추다” 기능을 가진 프로토콜로 나누어 해결해야 한다.물론 무차별적으로 프로토콜을 분리하며 구현하면 코드가 복잡해질 수 있기 때문에 기준을 잘 잡고 프로토콜 하나에 필요한 기능들만 적절히 구현해줘야 한다.
ISP 한 줄 결론
ISP를 위반하지 않으려면 Protocol을 구현할 때 SRP와 LSP를 잘 지켜라!
모든 피드백 감사합니다. (꾸벅)'Refactoring' 카테고리의 다른 글
[SOLID] DIP에 대해 알아보자 (1) 2024.10.08 [SOLID] OCP에 대해 알아보자 (2) 2024.10.08 [SOLID] SRP에 대해 알아보자 (7) 2024.10.08