JosephCha의 개발일지

SOLID 원칙 본문

iOS

SOLID 원칙

JosephCha 2023. 8. 9. 16:35
반응형
SOLID 원칙이란 객체 설계에 필요한 5가지 원칙으로써 유지보수가 쉽고, 유연하고, 확장이 쉬운 소프트웨어를 만들기 위한 수단, 높은 응집력과 낮은 결합도의 코드를 만들 수 있음

SRP (Single Responsibility Principle) : 단일 책임 원칙

  • 하나의 클래스는 하나의 책임을 가져야 함
  • 클래스의 수정이유는 단 하나여야 함
  • 하나의 책임이 여러개의 클래스에 나뉘어 있어서도 안됨
  • 하나의 클래스 안에 협력관계(Collaboration)가 여러개가 있는것은 괜찮음
  • 장점
    • SRP 원칙을 잘 따르면 한 책임의 변경으로부터 다른 책임의 변경으로의 연쇄작용에서 자유로울 수 있게 됨
    • 책임을 적절히 분배함으로써 코드의 가독성 향상, 유지보수 용이

Before

class Handler {
  func handle() {
    let data = requestDataToAPI()
    let array = parse(data: data)
    saveToDB(array: array)
  }
    
  private func requestDataToAPI() -> Data {
  // send API request and wait the response
  }
    
  private func parse(data: Data) -> [String] {
  // parse the data and create the array
  }
    
  private func saveToDB(array: [String]) {
  // save the array in a database (CoreData/Realm/...)
  }
}

하나의 class가 여러개의 func(책임)을 가지고 있음

After

class Handler {
    let apiHandler: APIHandler
    let parseHandler: ParseHandler
    let dbHandler: DBHandler
 
    init(apiHandler: APIHandler, parseHandler: ParseHandler, dbHandler: DBHandler) {
        self.apiHandler = apiHandler
        self.parseHandler = parseHandler
        self.dbHandler = dbHandler
    }
 
    func handle() {
        let data = apiHandler.requestDataToAPI()
        let array = parseHandler.parse(data: data)
        dbHandler.saveToDB(array: array)
    }
}
 
class APIHandler {
    func requestDataToAPI() -> Data {
        // send API request and wait the response
    }
}

class ParseHandler {
    func parse(data: Data) -> [String] {
        // parse the data and create the array
    }
}
 
class DBHandler {
    func saveToDB(array: [String]) {
        // save the array in a database (CoreData/Realm/...)
    }
}

하나의 클래스가 하나의 책임을 가지고있음. Handler 클래스의 경우는 협력관계인 여러 클래스를 활용하여 하나의 책임을 가지고 있음

OCP (Open-Closed Principle) : 개방,폐쇄 원칙

  • 기능 확장에는 열려있으나 기존 코드 변경에는 닫혀 있어야 함
    • 어떤 기능을 추가할 때, 기존의 코드는 만지지 않고 새로 동작하는 기능에 대해서만 확장되도록 코드가 작성이 되어야 함
  • 장점
    • 추가된 기능 관련 코드로 인한 기존 코드 수정이 없어, 유지보수 용이해짐

Before

enum 게임 {
  case 스포츠게임
  case 아케이드게임
  case FPS게임
}

class Flag {
  let 게임유형: 게임
    
  init(게임유형: 게임) {
    self.게임유형 = 게임유형
  }
}

func printNameOf게임(flag: Flag) {
  switch flag.게임유형 {
    case .스포츠게임:
      print("FIFA")
    case .아케이드게임:
      print("크레이지아케이드")
    case .FPS게임:
      print("오버워치")
  }
}

enum에 새로운 case를 추가하려면 printNameOf게임 함수도 수정해야함 → 유지보수 어려움

After

protocol 게임 {
  var name: String { get }
}

struct 스포츠게임: 게임 {
  let name: String = "피파"
}

struct 아케이드게임: 게임 {
  let name: String = "크레이지아케이드"
}

struct FPS게임: 게임 {
  let name: String = "오버워치"
}

class Flag {
  let 게임유형: 게임
  
  init(게임유형: 게임) {
    self.게임유형 = 게임유형
  }
}

func printNameOf게임(flag: Flag) {
  print(flag.게임유형.name)
}

다른 struct를 추가하고 싶다면 ‘게임’프로토콜을 채택하는 struct만 만들면 됨 → 유지보수 용이

LSP (Liskov Substitution Principle) : 리스코프 치환 원칙

  • 프로그램 객체(인스턴스)는 프로그램의 정확성을 해치지 않으며, 하위 타입의 인스턴스로 바꿀 수 있어야 함
    • 부모(super class)의 자리에 자식(sub class)로 그대로 치환해도 문제 없이 돌아 가야함
    • 자식 클래스를 구현할 때, 기본적으로 부모 클래스의 기능이나 능력들을 물려받음. 여기서 자식 클래스는 동작을 할 때, 부모 클래스의 기능들을 제한하면 안된다는 뜻

Before

class Rectangle {
    var width: Float = 0
    var height: Float = 0
    
    var area: Float {
        return width * height
    }
}

class Square: Rectangle {
    override var width: Float {
        didSet {
            height = width
        }
    }
}

func printArea(of rectangle: Rectangle) {
	rectangle.height = 3
	rectangle.width = 6
	print(rectangle.area)
}

let rectangle = Rectangle()
printArea(of: rectangle)
// 18

let square = Square()
printArea(of: square)
// 36

정사각형은 직사각형을 상속받음. 하지만 자식인 정사각형의 height = width라는 구문으로 인해 printArea(_: Rectangle)에서 원하는 결과를 얻지 못하게 됨. 부모의 역할을 자식이 대신하지 못하고 있음

After

protocol Shape {
    var area: Float { get }
}

class Rectangle: Shape {
    let width: Float
    let height: Float
    
    var area: Float {
        return width * height
    }
    
    init(width: Float,
         height: Float) {
        self.width = width
        self.height = height
    }
}

class Square: Shape {
    let length: Float
    
    var area: Float {
        return length * length
    }
    
    init(length: Float) {
        self.length = length
    }
}

직사각형, 정사각형 모두 사각형이라는 프로토콜을 채택해, 각각 다르게 사각형 프로토콜의 넓이를 구하는 로직을 구현하면 됨

ISP(Interface Segregation Principle) - 인터페이스 분리 원칙

  • 일반적인 인터페이스보단 각각의 구체적인 인터페이스로 분리하는 것이 좋다는 원칙
  • 사용하는 기능만을 가지는 인터페이스를 채택하자는 것
  • 인터페이스가 거대해지는 경우 SRP를 어기는 경우가 생길 수 있고, 해당 인터페이스를 채택해서 사용하는 경우 쓰지 않는 메소드가 있어도 넣어야 하는 경우가 발생할 수 있으니 최대한 인터페이스를 분리하는 것을 권장
  • 장점
    • ISP를 통해 시스템의 내부 의존성을 약화시켜 리팩토링, 수정, 재배포를 쉽게 할 수 있음

Before

protocol GestureProtocol {
  func didTap()
  func didLongTap()
  func didDoubleTap()
}

class GestureButton: GestureProtocol {
  func didTap() {}
  func didLongTap() {}
  func didDoubleTap() {}
}

class DoubleTapButton: GestureProtocol {
  func didDoubleTap() {}
  
  // Useless
  func didTap() {}
  func didLongTap() {}
}

DoubleTapButton 클래스 내부에 사용하지 않는 메소드가 들어 있음

After

protocol TapGestureProtocol {
  func didTap()
}

protocol LongTapGestureProtocol {
  func didLongTap()
}

protocol DoubleTapGestureProtocol {
  func didDoubleTap()
}

class GestureButton: TapGestureProtocol, LongTapGestureProtocol, DoubleTapGestureProtocol {
  func didTap() {}
  func didLongTap() {}
  func didDoubleTap() {}
}

class DoubleTapButton: GestureProtocol {
  func didDoubleTap() {}
}

class LongAndTapButton: LongTapGestureProtocol, TapGestureProtocol {
  func didTap() {}
  func didLongTap() {}
}

func doSomething(button: DoubleTapGestureProtocol & LongTapGestureProtocol) {
  button.didDoubleTap()
  button.didLongTap()
}

프로토콜을 분리함으로써 필요한 프로토콜만 채택하게되어 사용하지 않는 메소드가 없어짐

DIP (Dependency Inversion Principle): 의존관계 역전 원칙

  • 상위 계층이 하위 계층에 의존하는 전통적인 의존관계를 반전(역전)시킴으로써 상위 계층이 하위 계층의 구현으로부터 독립되게 할 수 있음
    • 상위레벨 모듈은 하위레벨 모듈에 의존하면 안됨. 두 모듈은 추상화된 인터페이스(프로토콜)에 의존해야함
    • 의존 관계를 맺을 때 변화하기 쉬운 것 또는 자주 변화하는 것(클래스) 보다는 변화하기 어렵거나 거의 변화가 없는 것(인터페이스)에 의존해야함
    • 추상화 된 것은 구체적인 것에 의존하면 안되고, 구체적인 것이 추상화된 것에 의존해야 함
    • 하위레벨 모듈이 상위레벨 모듈을 참조하는 것은 되지만 상위레벨 모듈이 하위레벨 모듈을 참조하는 것은 안 하는게 좋음. 그런 경우는 제네릭이나 Associate를 사용
    • 장점
      • 변경되기 쉬운것에 의존하지 않기 때문에, 변경이 잦아도 코드에서 변경해야할 부분이 적음
        • 의존성을 Interface에 두면 실제 구현 클래스의 구현이 변경되어도 이를 사용한 코드는 큰 영향을 받지 않음
      • Testable한 코드 작성에 용이: 핵심체는 Protocol에만 의존하기 때문에 Protocol의 구현체를 갈아끼워, Testable 코드 작성에 용이

Before

class 맥북13인치 {
  func 전원켜기() {}
}

class 개발자 {
  let 노트북: 맥북13인치 = 맥북13인치()
  
  func 개발시작() {
    노트북.전원켜기()
  }
}

변화하기 쉬운 모듈(클래스)에 의존함

의존성 주입(DI)

class 맥북13인치 {
  func 전원켜기() {}
}

class 개발자 {
  let 노트북: 맥북13인치
  
  init(노트북: 맥북13인치) {
    self.노트북 = 노트북
  }
  
  func 개발시작() {
    노트북.전원켜기()
  }
}

개발자 클래스는 의존성 주입을 통해 맥북 13인치 클래스를 의존

After

//제어의 주체 - 객체의 생성과 사용의 관심을 분리하는것
protocol 노트북 {
  func 전원켜기()
}

//아래의 클래스들과 의존관계가 생길 클래스 
class 개발자 {
  let 노트북: 노트북 // 프로토콜 
  
  init(맥북: 노트북) {
    self.노트북 = 맥북
  }
  
  func 개발시작() {
    노트북.전원켜기()
  }
}

//평범한 클래스들이지만 위의 인터페이스에 의존관계가 있는 클래스들
class 맥북13인치: 노트북 {
  func 전원켜기() {}
}

class 맥북15인치: 노트북 {
  func 전원켜기() {}
}

class 레노버: 노트북 {
  func 전원켜기() {}
}

// 외부에서 의존성 주입 - 맥북 13인치 클래스와 개발자 클래스는 의존관계가 있다.
let iOS개발자 = 개발자(노트북 : 맥북13인치())

개발자 클래스가 노트북이라는 프로토콜을 의존함으로 써, 구현을 하위 모듈에게 맡김 (개발자 클래스의 재사용성이 높아짐)

연관 타입(Associate Type)의 활용

Before

protocol 노트북 {
    func 전원켜기()
    func 앱스토어(맥북: 맥북13인치)
}

class 맥북13인치: 노트북 {
    func 전원켜기() {}
    func 앱스토어(맥북: 맥북13인치) {
        print("앱스토어에 접근")
    }
}

상위레벨 모듈(노트북)이 하위레벨 모듈(맥북13인치)을 의존하여 순환이 발생 (DIP 원칙 위배)

After

protocol 노트북 {
    func 전원켜기()
    func 앱스토어(맥북: MyType)
    associatedtype MyType
}

class 맥북13인치: 노트북 {
    func 전원켜기() {}
    func 앱스토어(맥북: 맥북13인치) {
        print("앱스토어에 접근")
    }
}

class 맥북15인치: 노트북 {
    func 전원켜기() {}
    func 앱스토어(맥북: 맥북15인치) {
        print("앱스토어에 접근")
    }
}

Associate Type을 활용해서 상위레벨 모듈에서 하위레벨 모듈의 의존을 끊음. 하위레벨 모듈에서 앱스토어() 함수에 인자를 넣을 때 원하는 타입을 넣으면 됨

 

SOLID 원칙을 지키지 않았을 때 발생하는 문제점

  1. 경직성 : 하나를 바꿀 때 다른것들도 바꿔야해서 시스템을 변경하기 어려움
  2. 부서지기 쉬움 : 한 부분이 변경되었을 때 다른 한 부분이 영향을 받아서 새로운 오류가 발생함
  3. 부동성 : 다른 시스템이 재사용하기 힘듬
  4. 점착성 : 제대로 작동하기 어려움
  5. 불필요한 복잡성 : 과도한 설계
  6. 불필요한 반복
  7. 불투명성 : 의도를 파악하기 어려운 혼란스러운 표현

참고: https://hellozo0.tistory.com/373

https://medium.com/@jgj455/오늘의-swift-상식-객체와-solid-원칙-270415c64b64

https://ios-development.tistory.com/710

'iOS' 카테고리의 다른 글

Coordinator 패턴  (1) 2023.08.09
iOS MVVM 패턴  (0) 2023.08.09
네트워크 Endpoint  (0) 2023.08.09
iOS 동작과정  (0) 2023.08.09
UIKit 기본개념  (0) 2023.08.09
Comments