일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 |
- gitlabci/cd
- 애플인텔리전스
- Content Hugging priority
- IOS
- apple intelligence
- OperationQueue
- Union-Find
- Content Compression Resistance priority
- cleanarchitecture
- CI/CD
- 동작과정
- 동시성프로그래밍
- 자료구조
- 클린아키텍처
- AI
- rxswift
- swift
- LLM
- ReactiveX
- RxSwift요약
- gitlab
- 백준
- CICD
- 알고리즘
- swift알고리즘
- 오토레이아웃
- ai expo
- Autolayout
- RxCocoa
- mvvm
- Today
- Total
JosephCha의 개발일지
SOLID 원칙 본문
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 원칙을 지키지 않았을 때 발생하는 문제점
- 경직성 : 하나를 바꿀 때 다른것들도 바꿔야해서 시스템을 변경하기 어려움
- 부서지기 쉬움 : 한 부분이 변경되었을 때 다른 한 부분이 영향을 받아서 새로운 오류가 발생함
- 부동성 : 다른 시스템이 재사용하기 힘듬
- 점착성 : 제대로 작동하기 어려움
- 불필요한 복잡성 : 과도한 설계
- 불필요한 반복
- 불투명성 : 의도를 파악하기 어려운 혼란스러운 표현
참고: https://hellozo0.tistory.com/373
https://medium.com/@jgj455/오늘의-swift-상식-객체와-solid-원칙-270415c64b64
'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 |