관리 메뉴

김종권의 iOS 앱 개발 알아가기

[Clean Architecture] 3. 코드로 알아보는 SOLID - LSP(Liskov Substitution Principle) 리스코프 치환 원칙 본문

Clean Architecture/Clean Architecture 코드

[Clean Architecture] 3. 코드로 알아보는 SOLID - LSP(Liskov Substitution Principle) 리스코프 치환 원칙

jake-kim 2021. 9. 18. 23:49

0. 코드로 알아보는 SOLID - 클래스 다이어그램 필수 표현

1. 코드로 알아보는 SOLID - SRP(Single Responsibility Principle) 단일 책임 원칙

2. 코드로 알아보는 SOLID - OCP(Open Close Principle) 개방 폐쇄 원칙

3. 코드로 알아보는 SOLID - LSP(Liskov Substitution Principle) 리스코프 치환 원칙

4. 코드로 알아보는 SOLID - ISP(Interface Segregation Principle) 인터페이스 분리 원칙

5. 코드로 알아보는 SOLID - DIP(Dependency Inversion Principle, testable) 의존성 역전 원칙

6. 코드로 알아보는 SOLID - Coordinator 패턴 화면전환

7. 코드로 알아보는 SOLID - Network, REST (URLSession, URLRequest, URLSessionDataTask)

8. 코드로 알아보는 SOLID - 캐싱 Disk Cache (UserDefeaults, CoreData)

LSP

  • Liskov Substitution Principle
  • 리스코프 치환 원칙
  • A가 사용하고 있는 B하위 타입이 무엇으로 바뀌든지 A에 영향을 주지 않는 상태

  • 상속을 사용할 땐 다른 구현체로 갈아끼워도 A에 아무 영향을 주지 않는 것

주의) LSP를 위반하는 예제

  • Rectangle의 width, height는 각 독립으로 사용되어서, 자식으로 직사각형만 올 수 있지만, 정사각형(square)는 사용할 수 없는 상태

코드로 알아보는 LSP

  • Billing 앱: 현재 지출한 금액을 기준으로 계산을 해주고 view에 표출하는 앱

  • 비즈니스 요구사항
    • License별로 금액을 다르게 계산
    • 1) personal은 사용한 금액에 * 1.2를 하여 세금 포함된 가격으로 계산
    • 2) Business는 사용한 금액에 * 0.7를 하여 혜택을 부여한 가격으로 계산
    • 현재 1)번과 2)번의 요구사항은 정해지지 않았고 자꾸 바뀌는 상태 > 두 비즈니스 로직을 변경하기 쉽게 LSP를 사용

  • 자주 변경될거 같은 비즈니스 로직을 사용하는 부분은 Interface로 하고, 쉽게 갈아끼울수 있는 구현체들로 구성
    • 테스트) random값을 이용하여 Personal 로직이나 Business로직이 나오도록 분리
// DI
let randomInt = Int.random(in: (0...1))
let useCase: LicenseUseCase
if randomInt == 0 {
    useCase = PersonalUseCase()
} else {
    useCase = BusinessUseCase()
}
let viewModel = BillingViewModelImpl(licenseUseCase: useCase)
let viewController = BillingViewController(viewModel: viewModel)

navigationController?.pushViewController(viewController, animated: true)
  • LicenseUseCase 정의
protocol LicenseUseCase {
    func calcFee(_ currentFee: Int) -> Double
}
  • PersonalUseCase, BusinessUseCase 정의
class PersonalUseCase: LicenseUseCase {
    func calcFee(_ currentFee: Int) -> Double {
        return Double(currentFee) * 1.2
    }
}
class BusinessUseCase: LicenseUseCase {
    func calcFee(_ currentFee: Int) -> Double {
        return Double(currentFee) * 0.7
    }
}
  • BillingViewModel 정의
protocol BillingViewModelInput {
    func didTapButton()
}

protocol BillingViewModelOutput {
    var licenseInfo: BehaviorRelay<String> { get }
}

protocol BillingViewModel: BillingViewModelInput, BillingViewModelOutput {}

class BillingViewModelImpl: BillingViewModel {

    let licenseUseCase: LicenseUseCase

    init(licenseUseCase: LicenseUseCase) {
        self.licenseUseCase = licenseUseCase
    }

    // Output

    let licenseInfo: BehaviorRelay<String> = .init(value: "")

    // Input

    func didTapButton() {
        // 현재 사용한 금액을 2000원이라 가정
        let result = licenseUseCase.calcFee(2000)
        licenseInfo.accept("계산된 최종 금액 = \(result)")
    }

}
  • BillingViewController 정의
class BillingViewController: UIViewController {

    var viewModel: BillingViewModel!
    let disposeBag = DisposeBag()

    lazy var feeButton: UIButton = {
        let button = UIButton()
        button.setTitle("fee 계산하기", for: .normal)
        button.setTitleColor(.gray, for: .normal)
        button.backgroundColor = .systemGray.withAlphaComponent(0.5)

        return button
    }()

    lazy var billingListContainerStackView: UIStackView = {
        let view = UIStackView()
        view.spacing = 16.0

        return view
    }()

    lazy var resultLabel: UILabel = {
        let label = UILabel()
        label.textColor = .gray

        return label
    }()

    init(viewModel: BillingViewModel) {
        super.init(nibName: nil, bundle: nil)

        self.viewModel = viewModel
    }

    required init?(coder: NSCoder) {
        fatalError()
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        setupViews()
        addSubviews()
        makeConstraints()
        bindInput()
        bindOutput()
    }

    private func setupViews() {
        view.backgroundColor = .white
    }

    private func addSubviews() {
        view.addSubview(feeButton)
        view.addSubview(resultLabel)
    }

    private func makeConstraints() {
        feeButton.snp.makeConstraints { maker in
            maker.top.equalTo(view.safeAreaLayoutGuide.snp.top).offset(120)
            maker.centerX.equalToSuperview()
        }

        resultLabel.snp.makeConstraints { maker in
            maker.top.equalTo(feeButton.snp.bottom).offset(30)
            maker.centerX.equalTo(feeButton)
        }
    }

    private func bindInput() {
        feeButton.rx.tap.subscribe(onNext: { [weak self] in self?.didTapButton() }).disposed(by: disposeBag)
    }

    private func didTapButton() {
        viewModel.didTapButton()
    }

    private func bindOutput() {
        viewModel.licenseInfo.subscribe(onNext: { [weak self] in self?.resultLabel.text = $0 }).disposed(by: disposeBag)
    }
}

* 전체 소스 코드 `LSP 부분`: https://github.com/JK0369/SOLID

Comments