Youngminah/TIL

SOLID 객체지향의 5가지 원칙

Youngminah opened this issue · 1 comments

image

  • 면접 단골 질문이자,
  • 꼭 알아야하는 객체지향의 5원칙..
  • TCP/IP 4계층 같은 느낌쓔,,
  • 이 정도 개념은 아가수준이야..


SOLID

객체 설계에 필요한 5가지 원칙으로써 유지보수가 쉽고, 유연하고, 확장이 쉬운 소프트웨어를 만들기 위한 수단으로 본다.

객체의 책임이란

  • Knowing, Doing으로 나뉜다.
    image




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

  • 클래스 수정이유는 단 하나여야 한다.
  • 하나의 클래스는 하나의 책임(Knowing, Doing)만 가져야 한다.
  • 하나의 책임이 여러개의 클래스에 나뉘어 있어서도 안된다.
  • 하나의 클래스 안에 협력관계(Collaboration)가 여러개 있는것은 괜찮다.

SRP 적용전

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

SRP 적용후

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




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

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

OCP 적용전

enum Country {
  case korea
  case japan
  case china
}

class Flag {
  let country: Country
  
  init(country: Country) {
    self.country = country
  }
}

func printNameOfCountry(flag: Flag) {
  switch flag.country {
    case .china:
      print("중국")
    case .korea:
      print("한국")
    case .japan:
      print("일본")
  }
}
  • Country 열거형에 USA case를 추가하면 printNameOfCountry() 함수도 수정해야 한다. 결합도와 의존성이 높고, 유지보수가 힘들다.

OCP 적용후

enum Country {
  case korea
  case japan
  case china
    
  var name: String {
    switch self {
      case .china:
        return "중국"
      case .korea:
        return "한국"
      case .japan:
        return "일본"
    }
  }
}

class Flag {
  let country: Country
    
  init(country: Country) {
    self.country = country
  }
}

func printNameOfCountry(flag: Flag) {
  print(flag.country.name)
}
protocol Country {
  var name: String { get }
}

struct Korea: Country {
  let name: String = "한국"
}

struct Japan: Country {
  let name: String = "일본"
}

struct China: Country {
  let name: String = "중국"
}

class Flag {
  let country: Country
  
  init(country: Country) {
    self.country = country
  }
}

func printNameOfCountry(flag: Flag) {
  print(flag.country.name)
}
  • USA를 추가하고 싶다면 Country를 채택하는 구조체를 만들기만 하면 된다. 결합도가 낮고, 응집도가 높아 유지보수에 용이하다




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

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

LSP 적용전

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

LSP 적용후

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 변의길이 * 변의길이
  }
}




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

  • 인터페이스를 일반화하여 구현하지 않는 인터페이스를 채택하는 것보다
  • 구체적인 인터페이스를 채택하는 것이 더 좋다는 원칙.
  • 인터페이스를 설계할 때, 굳이 사용하지 않는 인터페이스는 채택하여 구현하지 말고
  • 오히려 한 가지의 기능만을 가지더라도 정말 사용하는 기능만을 가지는 인터페이스로 분리하라는 것
  • 추상적이니까 예제로 바로 설명 GO!

ISP 적용전

protocol Shape {
    var area: Float { get }
    var length: Float { get }
}

class Square: Shape {
    var width: Float
    var height: Float
    
    var area: Float {
        return width * height
    }
    
    var length: Float {
        return 0
    }
    
    init(width: Float,
         height: Float) {
        self.width = width
        self.height = height
    }
}

class Line: Shape {
    var pointA: Float
    var pointB: Float
    
    var area: Float {
        return 0
    }
    
    var length: Float {
        return pointA - pointB
    }
    
    init(pointA: Float,
         pointB: Float) {
        self.pointA = pointA
        self.pointB = pointB
    }
}
  • Line, Square 모두 Shape을 상속받는 객체이지만 실제로 Square는 length라는 변수가 필요가 없고
  • Line은 area라는 변수가 필요없게 됨
  • 하지만 예제에서는 단지 Shape이라는 프로토콜을 채택한다는 이유만으로 필요없는 기능을 구현하고 있음.
  • 이런 경우에 ISP의 원칙을 지키지 않고 있다고 할 수 있음!

ISP 적용후

protocol AreaCalculatableShape {
    var area: Float { get }
}

protocol LenghtCalculatableShape {
    var length: Float { get }
}

class Square: AreaCalculatableShape {
    var width: Float
    var height: Float
    
    var area: Float {
        return width * height
    }
    
    init(width: Float,
         height: Float) {
        self.width = width
        self.height = height
    }
}

class Line: LenghtCalculatableShape {
    var pointA: Float
    var pointB: Float
    
    var length: Float {
        return pointA - pointB
    }
    
    init(pointA: Float,
         pointB: Float) {
        self.pointA = pointA
        self.pointB = pointB
    }
}
  • 기존에 필요없는 기능들을 구현하고 있던 인터페이스들을 더욱 세분화하여 나누어주었음.




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

  • 상위레벨 모듈은 하위레벨 모듈에 의존하면 안된다.
  • 두 모듈은 추상화된 인터페이스(프로토콜)에 의존해야한다.
  • 추상화 된 것은 구체적인 것에 의존하면 안되고, 구체적인 것이 추상화된 것에 의존해야 한다.
  • 의존 관계를 맺을 때 변화하기 쉬운 것 또는 자주 변화하는 것(클래스) 보다는 변화하기 어렵거나 거의 변화가 없는 것(인터페이스)에 의존해야한다.
  • 하위레벨 모듈이 상위레벨 모듈을 참조하는 것은 되지만 상위레벨 모듈이 하위레벨 모듈을 참조하는 것은 안 하는게 좋다. 그런 경우는 제네릭이나 Associate를 사용
  • DIP 를 만족하면 의존성 주입이라는 기술로 변화를 쉽게 수용할 수 있다.

DIP 적용전

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

class 개발자 {
  let 노트북: 맥북13인치 = 맥북13인치()
  
  func 개발시작() {
    노트북.전원켜기()
  }
}
  • DI 의존성 주입 : 외부에서 객체를 생성해서 내부에 넣는것을 말함
class 맥북13인치 {
  func 전원켜기() {}
}

class 개발자 {
  let 노트북: 맥북13인치
  
  init(노트북: 맥북13인치) {
    self.노트북 = 노트북
  }
  
  func 개발시작() {
    노트북.전원켜기()
  }
}
  • 객체생성을 외부에서 한경우 이를 DI라고 부름
  • 그럼 DIP는 머냐?

DIP 적용후

protocol 노트북 {
  func 전원켜기()
}

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

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

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

class 레노버: 노트북 {
  func 전원켜기() {}
}
  • 객체를 외부에서 생성해서 주입할 뿐만아니라, 프로토콜이라는 추상화시킨 객체를 의존시키게 만들어야한다.
  • DIP는 더 중요한 모듈이 덜 중요한 모듈에 의존하면 안되며
  • 추상화에 의존해야 되는 원칙으로 실체에 의존할 것인가, 추상화에 의존할 것인가가 포인트라는 것!
  • DIP는 엄청 중요하기 때문에 밑에 따로 다시 정리하겟움 ❗️❗️

Dependency, 의존성이란?

  • 객체 지향 프로그래밍에서 Dependency, 의존성은 서로 다른 객체 사이에 의존 관계가 있다는 것을 말한다.
  • 즉, 의존하는 객체가 수정되면, 다른 객체도 영향을 받는다는 것이다.
import UIKit

struct Eat {
    func coffee() {
        print("아메리카노")
    }

    func meal() {
        print("피자")
    }
}

struct Person {
    var todayEat: Eat
    
    func coffee() {
        todayEat.coffee()
    }
    
    func meal() {
        todayEat.meal()
    }
}
  • Person객체는 Eat객체를 인스턴스로 사용하고 있으므로, Eat객체에 의존성이 생긴다.
  • 만약 이때, Eat 객체에 중요한 수정이나 오류가 발생한다면, Person 객체도 영향을 받을 수 있다.
  • 의존성을 가지는 코드가 많아진다면, 재활용성이 떨어지고 매번 의존성을 가지는 객체들을 함께 수정해 주어야 한다는 문제가 발생한다.
  • 이러한 의존성을 해결하기 위해 나온 개념이 바로 Dependency Injection, 의존성 주입이다.

Injection이란?

  • Injection, 주입은 외부에서 객체를 생성 ❗️❗️ 해서 넣는 것을 의미한다.
class Eat:Menu {
    var coffee: String
    var meal: String
    
    init(coffee: String, meal: String) {
        self.coffee = coffee
        self.meal = meal
    }
    
    func printCoffee() {
        print("아메리카노")
    }
    
    func printMeal() {
        print("피자")
    }
}
let menu = Eat(coffee: "아메리카노", meal: "피자")
  • 위 코드와 같이 생성자 등을 활용해서 외부에서 주입할 수 있다.

Dependency Injection 의존성 주입의 장점

  • Unit Test가 용이해진다. iOS 개발자들에게 필요한 테스트용이한 코드 작성하기의 핵심
  • 코드의 재활용성을 높여준다.
  • 객체 간의 의존성(종속성)을 줄이거나 없엘 수 있다.
  • 객체 간의 결합도이 낮추면서 유연한 코드를 작성할 수 있다.
  • 그렇다면 내부에서 만든 객체를 외부에서 넣어서 의존성을 주입하는 것은 어떻게?
  • 이전에 DIP: 의존 관계 역전 법칙을 알아야 한다.

Dependency Inversion Principle : 의존관계 역전 법칙

  • DIP, 의존 관계 역전 법칙은 객체 지향 프로그래밍 설계의 다섯가지 기본 원칙(SOLID) 중 하나이다.
  • 추상화 된 것은 구체적인 것에 의존하면 안되고 구체적인 것이 추상화된 것에 의존 해야한다.
  • 즉, 구체적인 객체는 추상화된 객체에 의존 해야 한다는 것이 핵심이다.
  • Swift에서 추상화된 객체는 Protocol이 있다.
  • 우리는 이 Protocol을 활용해서 의존성 주입을 구현하려고 한다.
  • 우선 Protocol을 활용해서 추상적인 객체를 만들어야 한다.
  • Menu라는 Protocol은 printCoffee()와 printMeal()함수를 가지고 있다.
protocol Menu {
    func printCoffee()
    func printMeal()
}

이후 Eat클래스는 Menu Protocol을 채택한 후,
Protocol에 정의한 함수를 실체화 시켜준다.

class Eat: Menu {
    var coffee: String
    var meal: String
    
    init(coffee: String, meal: String) {
        self.coffee = coffee
        self.meal = meal
    }
    
    func printCoffee() {
        print("아메리카노")
    }
    
    func printMeal() {
        print("피자")
    }
}

이제부터 중요한 부분이 나온다.
기존의 방식과 다르게 todayEat변수는 추상적인 객체인 Menu타입에 의존하게 된다.
여기서 changeMenu함수를 활용해서 의존성 주입을 시킬 수 있다.

struct Person {
    var todayEat: Menu
    
    func printCoffee() {
        todayEat.printCoffee()
    }
    
    func printMeal() {
        todayEat.printMeal()
    }
    
    mutating func changeMenu(menu: Menu) {
        self.todayEat = menu
    }
}

이렇게 구현한다면 Eat객체와 Person객체는 거의 독립적인 객체가 된다.
Eat 객체를 수정하거나 Person을 수정한다고 해서 상대 객체를 함께 수정해야 하는 문제를 방지할 수 있다.

let menu = Eat(coffee: "아메리카노", meal: "피자")
let anotherMenu = Eat(coffee: "라떼", meal: "햄버거")

var suhshin = Person(todayEat: menu)

suhshin.printCoffee() // print 아메리카노
suhshin.changeMenu(menu: anotherMenu)
suhshin.printCoffee() // print 라떼