Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SOLID - Liskov Substitution Principle #74

Open
simoniful opened this issue Nov 30, 2022 · 0 comments
Open

SOLID - Liskov Substitution Principle #74

simoniful opened this issue Nov 30, 2022 · 0 comments

Comments

@simoniful
Copy link
Owner

simoniful commented Nov 30, 2022

정의

1988년, 바바라 리스코프는 하위 타입을 아래와 같이 정의한다

여기에서 필요한 건 다음와 같은 치환 원칙이다
S 타입의 객체 o1 각각에 대응하는 T 타입 객체 o2가 있고
T 타입을 이용해서 정의한 모든 프로그램 P에서 o2 자리에 o1을 치환하더라도
P의 행위가 변하지 않는다면 S는 T의 하위 타입이다

자식 클래스 or 구현체를 만들 때, 상위 타입의 객체로 치환해도 문제가 없는지 파악해야 한다는 의미
부모 클래스를 상속한 자식 클래스는 LSP 원칙을 준수
인터페이스를 구현한 구현체 역시도 LSP 원칙을 준수

상속에 있어서 가장 중요한 기본 원칙을 제시하며
OCP를 가능하게 해주는 중요 원칙이다

상속 / 구현을 하도록 가이드 하는 예시

image

public class Billing {
  var user: User
  var license: License!
  
  init(user: User) {
    self.user = user
    let personelLicense = PersonelLicense()
    let businessLicense = BusinessLicense()
    self.license = businessLicense.checkUser(user: user) ? businessLicense : personelLicense
  }
  
  func printFee(_ currentFee: Int) {
    print(license.calcFee(currentFee))
  }
}

protocol License {
  func calcFee(_ currentFee: Int) -> Double
}

public class PersonelLicense: License {
  func calcFee(_ currentFee: Int) -> Double {
    return Double(currentFee) * 1.2
  }
}

public class BusinessLicense: License {
  private var users: [User] = [
    User(name: "Simon", age: 32, gender: true),
    User(name: "Jin", age: 30, gender: false),
    User(name: "Jack", age: 28, gender: true)
  ]
  
  func calcFee(_ currentFee: Int) -> Double {
    return Double(currentFee) * 0.8
  }
  
  func checkUser(user: User) -> Bool {
    return users.contains { member in
      member == user
    }
  }
}

public struct User: Equatable {
  private var name: String
  private var age: Int
  private var gender: Bool
  
  init(name: String, age: Int, gender: Bool) {
    self.name = name
    self.age = age
    self.gender = gender
  }
  
  public static func == (lhs: User, rhs: User) -> Bool {
    return lhs.name == rhs.name && lhs.age == rhs.age && lhs.gender == rhs.gender
  }
}

let firstBilling = Billing(user: User(name: "Simon", age: 32, gender: true))
firstBilling.printFee(2000)
// Prints "1600.0"

let secondBilling = Billing(user: User(name: "Son", age: 35, gender: true))
secondBilling.printFee(2000)
// Prints "2400.0"

위의 클래스 다이어그램을 swift 형식으로 구현해보았다
Billing 클래스는 License 타입의 프로퍼티를 가지고 사용을 하며
License 타입에는 calcFee(_ currentFee:) 라는 공통적으로 구성해야할 메서드가 있다

하위 타입으로는 PersonelLicense, BusinessLicense가 있으며
하위 타입은 서로 다른 알고리즘을 이용해서 비용을 계산한다
BusinessLicense에서는 고정된 데이터를 기반으로 User 리스트를 체크하는 메서드가 있다

Billing의 행위가 어떤 License 하위 타입을 사용하는지에 전혀 의존하지 않으며
하위 타입들은 모두 License 타입으로 치환 가능하다

정사각형 / 직사각형 문제

image

직사각형의 하위 타입으로 정사각형으로 구성하면 잘못된 상속으로 LSP를 위배하게 된다
User 입장에서는 Interface인 Reactangle의 성격을 생각하고 사용하지만,
Square에서의 성격이 다른 경우가 존재하면 오류가 발생한다
오류를 해결하기 위해서는 분기 처리를 통한 상당량의 별도 매커니즘이 User에 필요하게 되는 악순환 발생
ex. Reactangle의 높이와 너비는 서로 독립적으로 변경 가능 / Square의 높이와 너비는 반드시 함께 변경

var r: Reactangle = Square()
r.setW(5)
r.setH(2)
r.area() 
// User입장에서는 10을 기대하고 사용하겠지만, 값으로는 4로 나오는 현상

LSP를 지키지 않으면, 별도의 매커니즘이 추가되며 오염된 코드화
ex. if문을 이용하여 Reactangle인지 Square인지 파악하는 매커니즘이 User 로직에 추가되며,
User의 행위가 사용하는 타입에 의존하게되므로 결국 타입을 서로 치환할 수 없게 되는 현상 발생

위배 사례

큰 서비스를 구현한다고 생각해보자

택시 파견 서비스의 통합 어플리케이션 구축의 예시

  • 고객: 상황에 맞는 적합한 택시 유형 검색 및 선택 전송
  • 시스템: REST를 통해 선택된 택시를 데이터베이스에서 검색하고 해당 URI에 정보를 덧붙여 호출
  • 데이터베이스: REST 서비스의 URI 저장 및 보관

ex. 택시기사 밥의 파견 URI

purplecab.com/driver/Bob

시스템은 해당 URI에 파견에 필요한 정보를 붙이고 PUT 방식으로 호출

purplecab.com/driver/Bob
            /pickupAddress/24 Maple St.
            /pickupTIme/153
            /destination/ORD

다양한 택시 업체에서 동일한 인터페이스를 준수하도록 구성하는 것이 중요
예외가 발생할 경우, 별도의 매커니즘이 추가되며 다른 규칙을 구성해야한다

ex. ACME 라는 회사에서 필드 name을 축약하여 dest로 사용하는 경우

acme.com/driver/Joe
            /pickupAddress/26 Nap St.
            /pickupTIme/153
            /dest/ORD

파견 명령어 구성 모듈에 if 문장을 모두 추가하여 분기 처리 해야한다

if (friver.getDispatchURI().startsWith("acme.com"))...

하지만, 제네릭하게 공통점을 추려 구현할 부분에서
acme라는 단어를 직접 코드에 추가하면
에러의 가능성을 무궁무진하게 만들게 된다
또한, 직접적인 노출로 보안에도 문제가 발생한다

만약 두 회사가 합병되어 통합적인 시스템을 구성하는 경우에도 문제가 된다
여기서도 if 절도 분기를 생각하면 엉망진창이 된다

따라서 이러한 버그 발생 가능성으로 시스템을 격리해야한다
파견 URI를 Key로 사용하는 설정용 DB를 이용하는 파견 명령 생성 모듈을 별도로 만들어야 할 수도 있다
해당 설명은 swift의 Moya 서드파티를 생각하면 이해하기 쉽다
REST 서비스들의 인터페이스가 서로 치환 가능하지 않는 상황이 만들어지게 된다면
그만큼 복잡한 매커니즘을 구성해야함을 잊지말도록 하자

베어코드에서의 예시

NSString과 NSMutableString의 상속 관계 정의
상위 클래스 기능을 받아서 하위 클래스에서 기능을 확장한다는 건 무조건 좋은 상태는 아니다
확장의 여부 결정을 판단할 때 LSP를 위배하지 않도록 구성 필요하다

상속에 있어서 해선 안되는 경우

  • 부모의 행위를 자식이 거부 - 더 추상화된 클래스를 사용한 코딩에 어떤 자식 클래스로 해당 대상을 바꾸는 경우 동작하지 않을 수 있음
  • 퇴화 함수 - 상위 클래스의 메서드를 오버라이드 후 그 기능을 사용하지 못하도록 구성하는 경우

특정한 행위에 대한 상속에 있어서 주의
개, 고양이에 "걷기", "뛰기" 행위를 추가하였더라도, 해당 행위를 동물 레벨로 올릴 수 없다 - 어류, 고래 등 예외 존재
따라서 "걷기", "뛰기", "헤엄" 등 행위는 상속과 별도로 존재해야한다
ex. IOS에서 UIView 상속한 구현체를 치환하여 사용시에 의도하지 않은 동작이 발생하는 경우
매번 방어 코드를 작성해야 하는 경우 발생
하지만, 대부분의 경우 LSP를 준수하여 작성되었기에 프레임워크 사용에 있어서는 작업에 어려움이 없다

위반이 발생한 경우 모든 클래스에서 하위 클래스를 명시적으로 지정하여 코딩해야하고
OCP를 사용할 수 없게 되고 코드의 복잡도가 올라간다
부모 클래스가 자식 클래스를 알게 되는 경우도 발생한다

LSP를 준수한 경우 상위 클래스 기준으로 작성된 코드가 문제 없이 동작한다
추상화된 인터페이스 하나로 공통의 코드를 작성 가능하며
상속 및 준수한 하위 클래스를 일일이 고민하지 않고 코딩이 가능하다
이 때, Protocol 익스텐션을 swift에선 활용 가능하다

원칙적으로 모든 것을 준수한 코드는 불가능
trade-off가 필요하며, 이에 대한 문제 발생 여지를 파악하는 것이 중요
설계 자체에 고려를 하면서 더 크게 발생할 문제를 사전에 방지하는 것이 가능
복잡하고 이해할 수 없는 상속을 만들지 않도록 LSP가 역할한다

아키텍쳐인 관점에서 LSP

초창기에는 상속을 사용하는 가이드 방법 정도로 얕게 인식
시간이 지나 인터페이스와 구현체에 적용되는 설계 원칙으로 넓게 인식

인터페이스는 다양한 형태로 swift에서는 protocol의 형태로 나타나며
해당 인터페이스를 준수하는 클래스, 구조체 등 구현 구성
잘 정의된 구현체의 상호 치환 가능성을 바탕으로 아키텍쳐적인 부분에서 원칙 위배 시 발생하는 사이드 이펙트에 집중하여 설계 필요
가장 강한 수준의 커플링과 연관된 원칙이다 보니 차후에 수정시 많은 시간 / 비용 발생

결론

LSP는 아키텍처 수준까지 확장해야한다
치환 가능성을 조금이라도 위배하게 된다면 전체적인 아키텍쳐를 오염시킨다
이는 보다 코드를 스파게티처럼 만들기에 설계에 있어서 주의가 필요하다

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant