객체지향 설계에 더 좋은 아키텍쳐를 설계하기 위해 지켜야하는 원칙들이며, 5가지로 구성된다.
SRP (단일 책임 원리)
OCP (개방폐쇄의 원칙)
LSP (리스코브 치환의 법칙)
ISP (인터페이스 분리의 법칙)
DIP (의존성 역전의 원칙)
SOLID 원칙을 지킴으로써 기대할 수 있는 점은,
유지보수가 쉽고, 유연하고, 확장이 쉬운 소프트웨어를 기대할 수 있다!
서로 다른 모듈간의 상호 의존하는 정도 또는 연관된 관계
- 결합도가 높으면
- 모듈간의 의존하는 정도가 크기 때문에
- 다른 모듈에 영향 → 오류가 생길 때도 다른 모듈에 영향을 줌
⇒ 고로, 결합도는 낮을수록 좋다!
모듈 내부의 요소들 간의 기능적 연관성을 나타내는 척도
- 모듈이 얼마나 독립적으로 되어있는 정도
- 수정이나 오류가 발생했을 때 하나의 모듈 안에서 처리
⇒ 응집도 또한 낮을수록 유지보수가 용이해짐!
클래스나 함수를 설계할 때, 각 단위들은 단 “하나의 책임”만을 가져야 한다
- 하나의 클래스가 많은 메서드 및 역할을 가지게 된다면, 클래스 내부 함수끼리 강한 결합이 발생함
- 클래스 내에 수정 추가가 진행될 때 → 가독성 저하 + 유지보수 비용 증가
⇒ 각 클래스 별로 책임을 적절히 나눠서 응집도를 높이고 결합도를 낮추는 프로그램 설계
‼️ 클래스의 인스턴스 변수가 너무 많다.
‼️ 속성과 상관없는 메소드가 많다.
‼️ 클래스나 메소드를 설명하기 위해 ‘and’, ‘if’, ‘or’을 많이 사용한다.
class SRPViewController {
private func login(id: String, password: String) {}
private func requestLogin() {
// Call API
}
private func decodeUserInfo(data: Data) -> User {
// Decoding User Inform from Data
return User(name: "", age: 10)
}
private func saveLocalData() {
// Local Data
}
}
위에 예시는 SRPViewController가 너무 여러가지 역할을 동시에 진행하는 모습이 보인다.
→ 즉 클래스 내부에서 하나의 책임이 아닌 여러가지 책임을 동시에 지고 있는 SRP를 위반한 사례!
class SRPViewController {
let loginManager: LoginManager
let userManager: UserManager
let localManager: LocalManager
init(loginManager: LoginManager,
userManager: UserManager,
localManager: LocalManager) {
self.loginManager = loginManager
self.userManager = userManager
self.localManager = localManager
}
func handler() {
loginManager.requestLogin()
loginManager.login(id: "", password: "")
userManager.decodeUserInform(data: Data)
localManager.saveLocalData()
}
}
class LoginManager {
func login(id: String, password: String) {}
func requestLogin() {
// Call API
}
}
class UserManager {
func decodeUserInform(data: Data) -> User {
// Decoding User Inform from Data
return User(name: "", age: 10)
}
}
class LocalManager {
func saveLocalData() {
// Local Data
}
}
LoginManager, UserInfoManager, LocalManager의 각각 클래스가 네이밍에 맞게 역할과 책임을 가지고, SRPViewController의 역할과 책임을 분리 → 결합도가 낮아진다!
⇒ 습관적으로 특정 클래스 내에 있는 내용이 꼭 이곳에 있어야 하는지 끊임없이 의심
⇒ 의식적으로 메서드나 클래스의 내용을 최대한 간결하게 처리
⇒ but. 지나친 추상화는 오히려 가독성과 유지보수를 해치기에 → 어느 레벨까지 처리할지 고민!
- 10-200?
- 함수는 10줄 이내, 클래스는 200줄 이내로 만드는 것이라고 합니다.
‘클래스는 200줄’ 룰을 지키려면 프로퍼티나 함수, 메서드를 그냥 손이 가는 곳 아무데나 만들어서 쓸 수 없다. 꼭 얘가 이 곳에 있어야 하는 이유를 찾아야 한다. 해당 클래스에 있을 이유가 없으면 SRP 위반이다. 가장 쉽게 판단할 수 있는 방법 하나는, 함수나 메서드 내부에서 self의 프로퍼티나 메서드를 얼마나 쓰고 있는지 보는 것이다. 만약 하나도 쓰고 있지 않다면 그 클래스 안에 있을 이유가 전혀 없는 것이다. 또는 self의 호출 빈도가 적을수록 클래스와 연관성이 떨어지는 것이니 나중에 클래스를 리팩토링 하거나 다이어트 시켜야 할 상황이 오면 우선적으로 내쫓을 후보가 되는 것이다.
확장에는 열려있으나, 변경에는 닫혀있어야 한다.
→ 기존의 코드를 변경하지 않으며(closed) 기능을 추가할 수 있도록 설계 해야 한다(open)
⇒ 클래스의 본질적인 특징과 확장되는 부분에 대한 차이가 명확해야 한다.
💡 ✅ 체크 포인트‼️ 새로운 기능이나 케이스가 추가될 때마다 기존의 코드를 수정해야한다.
‼️ 자신의 속성보다는 외부의 속성을 의존하고 있지 않은가?
‼️ 인터페이스보다는 구현한 타입에 의존하고 있지 않은가?
아래 예시를 통해서 조금 더 필요성을 알아봅시다!

게임캐릭터 1 클래스가 있고 해당 캐릭터는 앞구르기와 공격하기 메서드가 있담!
근데 아래와 같은 두가지 상황이 일어난다면?

앞구리기 → 뒷구르기로 변경하니, 외부에서 호출하는 attack 메서드에 오류가 생깁니다!

게임캐릭터2를 추가하면 → 기존 attack 메서드는 사용하지 못하고, 불필요한 메서드만 늘어납니다
즉 OCP를 위반하게 되면
⇒ 클래스의 본질적인 특징 + 확장되는 부분을 구분해야 한다
Protocol을 사용해 인터페이스화Protocol 채택, 클래스 내 직접 구현protocol 캐릭터 {
func QPressed()
func WPressed()
} // 절대 변하지 않는 부분
class 게임캐릭터1: 캐릭터 {
func QPressed() {
앞구르기() // 확장되는 부분
}
func WPressed() {
공격하기() // 확장되는 부분
}
private func 앞구르기() {}
private func 공격하기() {}
}
💡 ✅ 체크 포인트부모 클래스가 동작하는 곳에서, 자식 클래스 인스턴스를 넣어줘도 대체가능 해야한다.
‼️ 자식클래스에 너무 많은 override가 구현되어 있다.
‼️ 수직적 확장과 수평적 확장 중 어느 것이 필요한 상황인지 생각해본다.
‼️ 상속을 하면 강한 결합도가 생기기 때문에 주의 !
⇒ 자식 클래스가 부모 클래스의 기능을 오버라이딩해서 기능을 변경하거나 제한하는 경우 다르게 나오면 위반!
class 직사각형 {
var 너비: Float = 0
var 높이: Float = 0
var 넓이: Float = 0 {
didSet {
return 너비 * 높이
}
}
}
class 정사각형: 직사각형 {
override var 너비: Float {
didSet {
높이 = 너비
}
}
}
func printArea(of 직사각형: 직사각형) {
직사각형.높이 = 5
직사각형.너비 = 2
print(직사각형.넓이)
}
let rectangle = 직사각형()
printArea(of: rectangle) //10
let square = 정사각형()
printArea(of: square) //4
위에 코드를 보면
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 변의길이 * 변의길이
}
}
but. LSP를 “절대” 어기지 않고 프로그래밍 하는것은 어렵다
→ 지나친 LSP는 비효율성을 얻고, 지나친 LSP 위반은 안정성을 잃는다!
ex) BaseViewController의 역할 등에서 많이 고민을 해봐야겠죠?
사용하지 않는 인스턴스는 구현하지 말아야 한다.
- Swift에서 프로토콜을 설계하다보면 다양한 메서드가 들어가고 → 몇몇개의 메서드가 필요하지 않는 경우
ISP 원칙을 위반했다고 본다- 결국, 프로토콜 뭉치를 더욱 더 상세하게 분리할 필요가 있는 것이다.
- 프로토콜을 경우에 맞게 더 분리하면, 낭비하는 메서드가 없게 된다.
- but. 얼마나 상세하게 분리할지는 프로젝트 복잡성이나 유지보수 측면에서 고민해 볼 관점.
@objc
protocol GestureProtocol {
func 탭()
@objc optional func 꾸욱누르기()
@objc optional func 더블탭()
}
class GestureBtn: GestureProtocol {
func 탭() {}
func 꾸욱누르기() {}
func 더블탭() {}
}
class DoubleTapBtn: GestureProtocol {
func 탭() {}
//사용하지 않는 함수
func 꾸욱누르기() {}
func 더블탭() {}
}
위에 코드는 DoubleTapBtn 클래스에서 꾸욱누르기, 더블탭을 구현하지 않았기에 ISP를 위반했다고 볼 수 있죠.
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.꾸욱누르기()
}
그래서 이런식으로 하나씩 다 나눠주면 되는데, 뭐 상황에 따라 잘 조절하면 된다!
상위 모듈은 하위 모듈을 의존하고 있으면 안되고, 두 모듈 모두 추상화에 의존한다.
- 클래스 사이에 의존관계는 존재할 수 밖에 없음
- but 구체적인 클래스끼리가 아닌, 그 사이에 최대한 추상화된 인터페이스를 활용해 의존
✅ 체크 포인트
‼️ 내부적으로 생성하는 하위 모듈이 존재하는가? (주입)
‼️ 상위레벨 모듈이 재사용 가능한가?
‼️ 하위레벨 모듈의 구체적인 타입이 존재하는가?

Before: 스마트폰이 늘어날 수록 유지보수가 어려워짐
After: 사용자는 스마트폰을 의존하고, 나머지 아래 기종들을 스마트폰 프로토콜을 구현하는 형태로 설계
class Order {
let menu = Menu()
}
class Menu {
var price: Int = 1000
}
let order = Order()
print(order.menu.price) // 1000 }
}
위에 코드에서 의존성을 주입하고
class Order {
let menu: Menu
init(menu: Menu) {
self.menu = menu
}
}
class Menu {
var price: Int = 1000
}
let menu = Menu()
let order = Order(menu: menu)
print(order.menu.price)
의존성 역전 원칙에 따라 의존성을 분리합니다
protocol Price {
var price: Int { get set }
}
class Order {
let menu: Price
init(menu: Price) {
self.menu = menu
}
}
class Menu: Price {
var price: Int = 1000
}
let menu = Menu()
let order = Order(menu: menu)
print(order.menu.price)
출처:https://github.com/i-colours-u/Design-Pattern-In-Swift/blob/main/contents/1-SOLID.md