ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [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
Designed by Tistory.