[회고] 신입 iOS 개발자가 되기까지 feat. 카카오 자세히보기

💻 CS/객체지향

SOLID in Swift (3): LSP(Liskov Substitution Principle)

inu 2021. 10. 31. 22:43
반응형

해당 게시글은 아래 Article을 참고하여 작성되었습니다.
https://medium.com/movile-tech/liskov-substitution-principle-96f15559e363

 

Liskov Substitution Principle

Third article of the series of five about SOLID and its use in Swift

medium.com

SOLID란?

SOLID는 5개의 프로그래밍 디자인 원칙의 앞글자를 딴 합성어이다. 각 디자인 원칙들은 소프트웨어의 이해와 발전뿐 아니라 유연성과 유지보수성을 높여준다. 이러한 원칙들은 교수이자 소프트웨어 엔지니어인 Robert C. Martin(Uncle Bob으로 많이 알려진)으로부터 소개되었다.

  • Single Responsibility Principle
  • Open-Closed Principle
  • Liskov Substitution Principle
  • Interface Segregation Principle
  • Dependency Inversion Principle.

오늘은 이중 LSP(Liskov Substitution Principle)에 대해 알아보겠다.


LSP(Liskov Substitution Principle)

LSP는 "자료형 S가 자료형 T의 하위형이라면 다른 프로그래밍적 변경 없이 자료형 T의 객체를 자료형 S의 객체로 교체할 수 있어야 한다."는 원칙이다.

 

예시를 보자.

class Rectangle {
    var width: Int
    var height: Int

    init(width: Int, height: Int) {
        self.width = width
        self.height = height
    }

    func area() -> Int {
        return width * height
    }
}

class Square: Rectangle {
    override var width: Int {
        didSet {
            super.height = width
        }
    }

    override var height: Int {
        didSet {
            super.width = height
        }
    }
}

// 코드출처 : https://medium.com/movile-tech/liskov-substitution-principle-96f15559e363
  • Rectangle class와 이를 상속한 Square class가 있다.
  • Square에서는 width와 height를 override 하고 있으며, didSet을 설정해 width 혹은 height가 set 될 때 다른 속성을 해당 값과 똑같이 set 하도록 해주었다.

이는 LSP를 위반하는 예시이다. 만약 Rectangle 객체의 height 속성을 7로 설정하고, width 속성을 5로 설정했다고 해보자. 이러한 Rectangle 객체의 area() 메서드 결과는 35이다. 이런 상황에서 Rectangle 객체가 Square 객체로 변경되었다고 생각해보자. 그럼 width 설정과 함께 height 속성이 함께 설정되면서 결과는 25가 된다.

func main() {
    let square = Square(width: 10, height: 10)

    let rectangle: Rectangle = square

    rectangle.height = 7
    rectangle.width = 5

    print(rectangle.area())
}

// 코드출처 : https://medium.com/movile-tech/liskov-substitution-principle-96f15559e363

이러한 변화가 별 문제가 없어보일 수도 있다. Square라는 사실을 알고만 있으면 정상적으로 동작하는 것으로 볼 수도 있기 때문이다. 하지만 추상화라는 개념에서 보면 우리는 해당 객체가 Rectangle인지, Square인지 전혀 모르고 있어야 한다. 우리는 그저 이를 Rectangle로만 판단해야 한다. 이는 Rectangle로 판단해도 전혀 문제가 발생해선 안된다. 하지만 위의 예시에선 Rectangle과 전혀 다르게 동작한다. 그래서 LSP를 위반한 것으로 보아야 한다.

 

해당 예시를 LSP를 지키도록 개선해보겠다.

protocol Geometrics {
    func area() -> Int
}

class Rectangle: Geometrics {
    var width: Int
    var height: Int

    init(width: Int, height: Int) {
        self.width = width
        self.height = height
    }

    func area() -> Int {
        return width * height
    }
}

class Square: Geometrics {
    var edge: Int

    init(edge: Int) {
        self.edge = edge
    }

    func area() -> Int {
        return edge * edge
    }
}

// 코드출처 : https://medium.com/movile-tech/liskov-substitution-principle-96f15559e363

Geometrics라는 새로운 protocol을 이들이 상속하도록 하여, 간단하게 문제를 해결했다. 이렇게 되면 우리가 객체를 Geometrics로 추상화하여 판단해도 문제 되지 않는다.

 

또 다른 예시를 보자.

class Shape {
    func doSomething() {
        // do something relate to shape that is irrelevant to this example, actually
    }
}

class Square: Shape {
    func drawSquare() {
        // draw the square
    }
}

class Circle: Shape {
    func drawCircle() {
        // draw the circle
    }
}

// 코드출처 : https://medium.com/movile-tech/liskov-substitution-principle-96f15559e363
  • doSomething() 메소드를 보유한 Shape class를 상속받아 Square, Circle class를 생성했다. 각 class는 자신을 draw 하는 메서드를 보유한다.
func draw(shape: Shape) {
    if let square = shape as? Square {
        square.drawSquare()
    } else if let circle = shape as? Circle {
        circle.drawCircle()
    }
}

// 코드출처 : https://medium.com/movile-tech/liskov-substitution-principle-96f15559e363
  • 외부에서 draw 메소드에서 Shape 객체를 받고, 이를 Square와 Cirecle로 형 변환한 뒤 활용한다.

이 역시 LSP를 위반한 대표적인 예시이다. Square, Circle 객체가 파라미터로 받은 상위 객체 Shape와 전혀 다르게 동작하고 있기 때문이다. 만약 하위 객체인 Square, Circle이 아닌 Shape 객체가 파라미터로 전달된다면 이는 동작하지 않을 것이다. (cf. 이는 OCP도 위반한 코드이다. 만약 새로운 Shape의 하위 class를 생성하려 한다면 어쩔 수 없이 draw 메서드의 내용도 변경해야 하기 때문이다.)

 

이를 LSP(뿐만아니라 OCP)를 지키도록 수정해보자.

protocol Shape {
    func draw()
}

class Square: Shape {
    func draw() {
        // draw the square
    }
}

class Circle: Shape {
    func draw() {
        // draw the circle
    }
}

// 코드출처 : https://medium.com/movile-tech/liskov-substitution-principle-96f15559e363

Shape를 protocol로 두고 이를 활용해 draw를 수행하도록 했다.

func draw(shape: Shape) {
    shape.draw()
}

이렇게 되면 draw도 Switch문 같은 복잡한 코드 작성 없이도 사용할 수 있다. (새로운 Shape가 생겨도 draw를 보유하고 있기 때문에 OCP도 지키게 된다.) LSP를 지키도록 코드가 수정되었다.

 

LSP는 프레임워크나 API를 작성할 때 특히 중요하다. 다른 개발자가 코드의 세부 동작을 알 필요가 없도록 해야 하기 때문이다. 이는 협업에서도 중요하게 작용할 것으로 느껴지니 더욱 신경 써서 작업해야겠다.

반응형