본문 바로가기
Study

[Swift OOP] SOLID - 쉽게 알아보는 LSP (리스코프 치환 원칙)

by 차코.. 2024. 10. 1.

들어가며

 

객체 지향 설계에서 자주 언급되는 SOLID

프로그램의 유지보수성과 확장성을 높이기 위해 꼭 필요한 원칙인데요.

 

SOLID 원칙
- SRP(Single Responsibility Principle): 단일 책임 원칙
- OCP(Open Closed Priciple): 개방 폐쇄 원칙
- LSP(Liskov Substitution Priciple): 리스코프 치환 원칙
- ISP(Interface Segregation Principle): 인터페이스 분리 원칙
- DIP(Dependency Inversion Principle): 의존성 역전 원칙

 

 

그 중에서도 이번 글에서는 SOLID 원칙 중 세번째에 해당하는 리스코프 치환 원칙(LSP)에 대해

그림과 Swift의 예시 코드로 설명해보겠습니다.

 

 


 

✅ LSP(리스코프 치환 원칙)의 필요성

 

 

LSP에 대해 알아보기 전에, 또 주인공 김아요 군을 모셔보겠습니다.

 

이번 글에서 김아요 군은 Bird 클래스를 상속받았는데요.

 

class Bird {
    func fly() {
        print("새가 날아갑니다.")
    }
}

 

 

새는 날 수 있다는 개념을 바탕으로 상위 클래스 Bird에는 fly() 메서드가 정의되어 있습니다.

 

사람은 날지 못해요

 

 

하지만 사람은 날지 못하겠죠..?

 

때문에, 사람인 김아요 군은 상위 클래스인 새가 기대했던 동작을 하지 못합니다.

 

// 상위 클래스: 새
class Bird {
    func fly() {
        print("새가 날아갑니다.")
    }
}

// 하위 클래스: 김아요
class KimAyo: Bird {
    override func fly() {
        print("김아요 군은 날 수 없습니다!")
    }
}

// 사용 예시
func makeBirdFly(bird: Bird) {
    bird.fly()
}

let bird = Bird()
makeBirdFly(bird: bird)  // "새가 날아갑니다."

let penguin = Penguin()
makeBirdFly(bird: penguin)  // "김아요 군은 날 수 없습니다!" -> LSP 위반

 

 

상위 클래스인 Bird는 하위 클래스가 모두 날 수 있는 동물이라는 것에 기대를 가지고,

fly() 함수를 갖고 있습니다.

 

그러나 KimAyo 클래스는 상위 클래스의 fly() 함수를 재정의하면서,

사람이기에 '날 수 있다'는 상위 클래스의 기대를 져버립니다.

 

이는 LSP 원칙 위반에 해당되는 행위입니다.

 

 


 

✅ LSP(리스코프 치환 원칙) 적용하기

 

LSP 원칙의 핵심은 다음과 같은데요,

 

LSP의 핵심
- 하위 클래스는 상위 클래스와 동일하게 동작해야 합니다.
- 상위 클래스의 인스턴스를 사용하는 코드는, 하위 클래스의 인스턴스를 대체해도 문제없이 동작해야 합니다.

 

 

즉, 상위 클래스는 하위 클래스에 대한 일관적인 기대를 갖고 있고,

하위 클래스는 상위 클래스의 동작을 그대로 이어 받거나 확장을 해야 합니다.

 

LSP를 지키면 상속 구조가 예측 가능하게 동작되기 때문에

코드의 유지 보수성이 향상됩니다.

 

 

 

다시 위의 예제로 돌아가, LSP 원칙에 맞게 분리해봅시다.

 

LSP를 준수하기 위해 사람과 새의 기능을 분리하여, 사람에게는 fly 기능을 제외해보았습니다.

 

 

// 날 수 있는 행동을 정의
protocol Flyable {
    func fly()
}

// 상위 클래스: 생물
class Creature {
    func walk() {
        print("걷습니다.")
    }
}

// 하위 클래스: 앵무새 (날 수 있는 생물)
class Parrot: Creature, Flyable {
    func fly() {
        print("앵무새가 날아갑니다.")
    }
}


// 하위 클래스: 사람 (날 수 없는 생물)
class KimAyo: Creature {
    func talk() {
        print("김아요는 대화합니다.")
    }
}

 

 

새가 가질 수 있는 공통적 특징인 Flyable을 프로토콜로 정의하고,

이 프로토콜에 해당하는 클래스만 채택하도록 설계했습니다.

 

이렇게 되면 KimAyo 클래스는 Flyable 인터페이스를 따르지 않기 때문에

더 이상 상위 클래스의 fly() 메서드를 재정의 하거나 상위 클래스의 계약을 위반하지 않게 됩니다.

 

각 클래스가 자신의 특성에 맞는 동작만 수행할 수 있게 된 것이죠!

 

 


마치며

 

이렇게 해서 SOLID의 원칙 중 LSP에 대해 알아보았는데요.

 

하위 클래스는 상위 클래스의 기능을 대체할 수 있어야 하며,

이를 통해 코드의 유연성과 안정성이 높아집니다.

 

감사합니다.