본문 바로가기
iOS/Swift

[iOS] SOLID 원칙 in Swift

by 안녕주 2022. 2. 19.

SOLID (객체 지향 설계)

SOLID란? 객체 지향 프로그래밍 및 설계의 다섯가지 기본 원칙입니다.

SOLID 원칙을 지킴으로써 유지보수가 쉽고, 유연하고, 확장이 쉬운 소프트웨어를 만들 수 있습니다.

  약어 개념
S SRP 단일 책임 원칙(Single Responsibility Principle) : 한 클래스는 하나의 책임만 가져야 한다.
O OCP 개방-폐쇄 원칙(Open/Closed Principle) : 소프트웨어 요소는 확장에는 열려있으나 변경에는 닫혀 있어야한다.
L LSP 리스코프 치환 원칙(Liskov Substitution Principle) : 프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야한다.
I ISP 인터페이스 분리 원칙(Interface Segregation Principle) : 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다
D DIP 의존관계 역전 원칙(Dependency Inversion Principle) : 프로그래머는 추상화에 의존해야지, 구체화에 의존하면 안된다. 의존성 주입은 이 원칙을 따르는 방법 중 하나다.

 

SRP : 단일 책임 원칙

모든 클래스는 하나의 책임만 가지며, 클래스는 그 책임을 완전히 캡슐화해야 한다.

  • 클래스의 수정이유는 단 하나여야한다.
  • 하나의 클래스는 하나의 책임을 가여쟈한다.
  • 하나의 책임이 여러개의 클래스에 나뉘어 있어서도 안된다.

 

before : 하나의 class가 여러개의 func(책임)을 가지고 있다.

class Server {
	func handle() {
    let data = requestDataToAPI()
    let array = parse(data: data)
    saveToDB(array: array)
  }
    
  private func requestDataToAPI() -> Data {
  }
    
  private func parse(data: Data) -> [String] {
  }
    
  private func saveToDB(array: [String]) {
  }
} 

after : 하나의 class가 하나의 func(책임)을 가지도록 분리

class Server {
	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 {
    }
}
 
class ParseHandler {
    func parse(data: Data) -> [String] {
    }
}
 
class DBHandler {
    func saveToDB(array: [String]) {
    }
}

 

 

OCP : 개방-폐쇄 원칙

소프트웨어 개체(클래스, 모듈, 함수 등등)는 확장에 대해 열려 있어야하고, 수정에 대해서는 닫혀 있어야한다.

  • 확장에는 열려 있으나, 변경에는 닫혀 있어야한다. (기능 케이스를 추가할 때도 기존 코드를 변경하지 않고 확장해야한다.)
  • 객체가 변경될 때는 해당 객체만 바꿔도 동작이 잘되면 OCP를 잘 지킨것이다.
  • 모듈이 주변환경에 지나치게 의존해서는 안된다.

 

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

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

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

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

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

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)
}

 

 

LSP : 리스코프 치환 원칙

프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야한다.

  • 서브타입은 상속받은 기본타입으로 대체 가능해야한다.
  • 자식 클래스는 부모 클래스 동작(의미)를 바꾸지 않는다.
  • 상속을 했을 때 서브클래스는 자신의 슈퍼클래스 대신 사용되도 같은 동작을 해야한다.

 

before : 사용자가 정사각형의 너비를 5로 생각했다면 잘못 동작한 것이다. 서브 클래스가 슈퍼클래스 대신 사용되도 같은 동작을 한다고 보기 어렵다.

class 직사각형 {
  var 너비: Float = 0
  var 높이: Float = 0
  var 넓이: Float {
    return 너비 * 높이
  }
}

class 정사각형: 직사각형 {
  override var 너비: Float {
    didSet {
      높이 = 너비
    }
  }
}

func printArea(of 직사각형: 직사각형) {
  직사각형.높이 = 5
  직사각형.너비 = 2
  print(직사각형.넓이)
}

let rectangle = 직사각형()
printArea(of: rectangle) //10
let square = 정사각형()
printArea(of: square) //4

after : 직사각형과 정사각형 모두 사각형의 조건을 만족한다.

protocol 사각형 {
  var 넓이: Float { get }
}

class 직사각형: 사각형 {
  private let 너비: Float
  private let 높이: Float
  
  init(너비: Float, 높이: Float) {
    self.너비 = 너비
    self.높이 = 높이
  }
  
  var 넓이: Float {
    return 너비 * 높이
  }
}

class 정사각형: 사각형 {
  private let 변의길이: Float
  
  init(변의길이: Float) {
    self.변의길이 = 변의길이
  }
  
  var 넓이: Float {
    return 변의길이 * 변의길이
  }
}

 

 

ISP : 인터페이스 분리 원칙

클라이언트가 자신이 이용하지 않는 메서드에 의존하지 않아야 한다는 원칙이다. ISP는 큰 덩어리의 인터페이스들을 구체적이고 작은 단위들로 분리시킴으로써 클라이언트들이 꼭 필요한 메서드들만 이용할 수 있게 한다. 이와 같은 작은 단위들을 인터페이스라고도 부른다. ISP를 통해 시스템의 내부 의존성을 약화시켜 리팩토링, 수정, 재배포를 쉽게 할 수 있다.

  • 클래스 내에서 사용하지 않는 인터페이스는 구현하지 말아야한다.
  • 클라이언트 객체는 사용하지 않는 메소드에 의존하면 안된다.
  • 인터페이스가 거대해지는 경우 SRP를 어기는 경우가 생길 수 있고, 해당 인터페이스를 채택해서 사용하는 경우 쓰지 않는 메소드가 있어도 넣어야 하는 경우가 발생할 수 있으니 최대한 인터페이스를 분리하는 것을 권장

 

before : DoubleTapBtn 함수에 사용하지 않는 함수가 존재한다.

protocol GestureProtocol {
  func 탭()
  func 꾸욱누르기()
  func 더블탭()
}

class GestureBtn: GestureProtocol {
  func 탭() {}
  func 꾸욱누르기() {}
  func 더블탭() {}
}

class DoubleTapBtn: GestureProtocol {
  func 탭() {}
	
	//사용하지 않는 함수
  func 꾸욱누르기() {}
  func 더블탭() {}

}

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

protocol TapGestureProtocol {
  func 탭()
}

protocol LongTapGestureProtocol {
  func 꾸욱누르기()
}

protocol DoubleTapGestureProtocol {
  func 더블탭()
}

class GestureBtn: TapGestureProtocol, LongTapGestureProtocol, DoubleTapGestureProtocol {
  func 탭() {}
  func 꾸욱누르기() {}
  func 더블탭() {}
}

class DoubleTapBtn: GestureProtocol {
  func 더블탭() {}
}

class LongAndTapBtn: LongTapGestureProtocol, TapGestureProtocol {
  func 탭() {}
  func 꾸욱누르기() {}
}

func doSomething(button: DoubleTapGestureProtocol & LongTapGestureProtocol) {
  button.더블탭()
  button.꾸욱누르기()
}

 

 

DIP : 의존관계 역전 원칙

상위 계층이 하위 계층에 의존하는 전통적인 의존관계를 반전(역전)시킴으로써 상위 계층이 하위 계층의 구현으로부터 독립되게 할 수 있다.

첫째, 상위 모듈은 하위 모듈에 의존해서는 안된다. 상위 모듈과 하위 모듈 모두 추상화에 의존해야한다.

둘째, 추상화는 세부 사항에 의존해서는 안된다. 세부샇랑이 추상화에 의존해야 한다.

DIP는 ‘상위와 하위 객체 모두가 동일한 추상화에 의존해야 한다’는 객체 지향적 설계의 대원칙을 따른다.

  • 상위레벨 모듈은 하위레벨 모듈에 의존하면 안된다.
  • 도 모듈은 추상화된 인터페이스(프로토콜)에 의존해야한다.
  • 추상화 된 것은 구체적인 것에 의존하면 안되고, 구체적인 것이 추상화된 것에 의존해야한다.
  • 하위레벨 모듈이 상위레벨 모듈을 참조하는 것은 되지만 상위레벨 모듈이 하위레벨 모듈을 참조하는 것은 안한는게 좋다. 그런 경우는 제너릭이나 Associate를 사용
  • DIP를 만족하면 의존성 주입이라는 기술로 변화를 쉽게 수용할 수 있다.

 

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

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

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

의존성 주입 : 개발자 클래스는 이제 맥북13인치 클래스를 의존

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

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

after : 의존성 분리는 의존관계 역전의 원칙(IOC)으로 의존관계를 분리합니다.

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

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

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

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

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

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

 

 

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

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

 

[참고블로그 & 꼭 아래의 블로그들도 같이 보면서 공부하세여!!! 꼭!!!]

객체지향 프로그래밍이란?

SOLID란?

[강추] SOLID in Swift

[강추]SOLID in Swift - 코드로 설명

Swift로 다시보는 객체지향 프로그래밍 : 피해야할 코딩 습관

'iOS > Swift' 카테고리의 다른 글

[iOS] Any, AnyObject, nil  (0) 2022.03.07
[iOS] ARC  (0) 2022.02.20
[iOS] 의존성 주입 DI  (2) 2022.02.19
[iOS] Data Binding in MVVM  (0) 2022.02.19
[iOS] MVVM 패턴  (0) 2022.02.18

댓글