SOLID 객체지향의 5가지 원칙
Youngminah opened this issue · 1 comments
Youngminah commented
- 면접 단골 질문이자,
- 꼭 알아야하는 객체지향의 5원칙..
- TCP/IP 4계층 같은 느낌쓔,,
- 이 정도 개념은 아가수준이야..
SOLID
객체 설계에 필요한 5가지 원칙으로써 유지보수가 쉽고, 유연하고, 확장이 쉬운 소프트웨어를 만들기 위한 수단으로 본다.
객체의 책임이란
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는 엄청 중요하기 때문에 밑에 따로 다시 정리하겟움 ❗️❗️
Youngminah commented
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 라떼