- hands-on으로 직접 짜보며 SOLID 원칙을 이해해봅시다!
- 읽으시는 분들, 좀 더 잘 이해하고 싶으시다면 같이 코드를 작성해보며 이해하는 것을 추천합니다 :)
SOLID란?
- 디자인 패턴의 일종으로 아래 다섯가지의 원칙을 지키는 것을 말합니다. 천천히 다섯가지 원칙을 쉬운 예시로 짚어보고자 합니다.
- Single Responsibility
- Open-Closed
- Liskov Substitution
- Interface Segregation
- Dependency Inversion
1. Single Responsibility
각각의 클래스는 하나의 책임만을 가져야 합니다.
클래스가 몇개의 책임을 맡고 있는지를 세어보고, 하나 이상의 책임을 가진 클래스들은 다른 클래스로 나누어 주는것이 좋습니다.
// 이 구조체에서는 두가지의 역할을 모두 맡고 있다.
struct Person {
let name: String
let age: Int
func checkAge() -> String {
if age < 20 {
return "미성년자"
} else {
return "성인"
}
}
}
let person = Person(name: "토털이", age: 27)
person.checkAge() // "성인"
- 이를 Single Responsibility 원칙을 따르게 바꾸어 보자.
// 하나의 구조체에 하나의 책임이 있고,
struct Person {
let name: String
let age: Int
}
// 또 다른 구조체에 하나의 책임이 있다
struct AgeVerifier {
func checkAge(age: Int) -> String {
if age < 20 {
return "미성년자"
} else {
return "성인"
}
}
}
let person = Person(name: "냥냥이", age: 1)
let verifyAge = AgeVerifier()
verifyAge.checkAge(age: person.age) // "미성년자"
- 이제 하나의 구조체가 하나의 역할만을 맡게 되었습니다.
2. Open-Closed
- 클래스는 확장(extension)에는 열려있지만 수정(modification)에는 닫혀있어야 합니다.
- 짧게 말해서, 새로운 기능을 전체 클래스를 수정하지 않고도 기능을 추가할 수 있어야 합니다. 새로운 기능을 위해 전체 클래스를 수정하는 건 개발과 테스트에 많은 시간을 요하기 때문입니다.
- 이제 이를 적용한 코드를 짜보면서 이해해볼 수 있습니다. 내가 동물원을 가졌다고 상상해보세요. 그리고 지금 동물원에는 사자 한마리만을 데리고 있습니다. 현실적으로 생각해보면, 더 많은 동물들이 있어야만 동물원에서 돈을 벌 수 있을 겁니다. 그렇기에 동물원을 지을 때 부터, 동물을 쉽게 추가할 수 있게끔 동물원을 지어주어야 합니다.
protocol AnimalProtocol {
func makeSound() -> String
}
class Tiger: AnimalProtocol {
func makeSound() -> String {
return "으르렁"
}
}
struct Zoo {
let animals: [AnimalProtocol]
func animalNoise() -> [String] {
return animals.map { $0.makeSound() }
}
}
let tiger = Tiger()
var zooAnimals = Zoo(animals: [tiger])
zooAnimals.animalNoise() // ["으르렁"]
- 이제 새로운 동물이 생겼고, 이를 동물원에 추가해야한다고 생각해봅시다. 간단하죠? 그냥 새로운 동물을 만들어서 동물원에 넣어주면 됩니다. 추후에 더 많은 동물들이 추가된다고 해도 똑같은 방법으로 추가해주면 됩니다. 아래 코드를 추가해줍시다.
class Horse: AnimalProtocol {
func makeSound() -> String {
return "끼로록"
}
}
let horse = Horse()
zooAnimals = Zoo(animals: [tiger, horse])
zooAnimals.animalNoise() // [roar, neigh]
3. Liskov Substitution
- 자식 클래스는 부모 클래스 타입의 정의를 깨트려서는 안됩니다.
- 부모(super class)로 동작하는 곳에서 자식(sub class)를 넣어주어도 대체가 가능해야합니다.
- 자식 클래스는 부모 클래스의 기능을 바꾸지 않고, 부모클래스의 메서드를 오버라이드 할 수 있어야 합니다. 이 원칙을 통해 더 재사용가능한 코드가 되고, 코드가 교환가능해집니다.
- 만일, 오버라이드 메서드가 아무것도 하지 않고 exception만 던진다면, Liskov Substitution을 위배하게 됩니다.
- 모두가 기존의 룰을 위반하지 않고 동작하는 프로그램이 만들어지게 되면, 이를 LSP를 지킨 설계라고 하게 됩니다.
// Bird, Eagle은 Liskov Substitution를 준수합니다.
class Bird {
func makeNoise() {
print("짹짹")
}
}
class Eagle: Bird {
override func makeNoise() {
print("으르렁-짹")
}
}
// 아래의 경우 부모 클래스를 깨뜨리기 때문에, Liskov Substitution를 위반하게 됩니다.
class Crow: Bird {
override func makeNoise() {
fatalError("내 소리가 뭐였더라?")
}
}
4. Interface Segregation
- 필요한 것만을 적용하라!
- 인터페이스를 분리함으로서, fat interface 문제를 해결할 수 있습니다. fat interface는 우리가 사용할 것보다 더 많은 메서드를 가지고 있는 경우를 말합니다.
- 필요한 것만을 사용하자!가 Interface Segregation의 핵심입니다.
- 아래의 코드는 Interface Segregation를 지키지 못한 예시입니다. 아기는 일을 하지 못하죠? 하지만 프로토콜을 따르려면 정의를 해줘야만 합니다. 이런경우 우리는 사용하지 않는 메서드를 구현해주게 됩니다.
protocol Action {
func eat()
func work()
}
class Adult: Action {
func eat() {
print("쩝쩝")
}
func work() {
print("아이고 허리야")
}
}
class Baby: Action {
func eat() {
print("냠냠")
}
func work() {
// baby can’t work
}
}
- 원칙을 지켜서 쓰려면 어떻게 해야할까?
- 아래와 같이 고쳐 써줄 수 있다.
protocol EatAction {
func eat()
}
protocol WorkAction {
func work()
}
class Adult: EatAction, WorkAction {
func eat() {
print("쩝쩝")
}
func work() {
print("아이고 허리야")
}
}
class Baby: EatAction {
func eat() {
print("냠냠")
}
}
5. Dependency Inversion
- 추상화에 의존하고, 구체적인 것에 의존하지 말자는 원칙입니다.
- 상위 모듈이 하위 모듈에 의존하면 안되고 두 모듈 모두 추상화에 의존하게 만들어야 한다는 원칙입니다.
- 높은 레벨의 모듈은 낮은 레벨의 모듈에 의존해서는 안됩니다. 예를 들어, 높은 레벨 모듈인 view controller은 networking 요소와 같은 하위 요소에 의존해서는 안됩니다. 대신, 추상화에 의존해야합니다. 스위프트 언어로는 프로토콜이라고 볼 수 있습니다. 커플링(coupling)을 줄이는게 이 규칙의 포인트입니다.
- 만일 firebase를 사용하다가 다음날 서버가 종료된다는 사실을 알았을 때, 바로 Realm과 같은 다른 서버리스에 연결을 해줘야합니다. 이런때, 상위모듈의 하위모듈의 관계를 떨어뜨려 놓았다면, 쉽게 의존을 대신해줄 수 있습니다.
- 아래의 코드는 하위 모듈인 NetworkRequest 모듈과 상위 모듈인DatabaseController가 관계가 깊다는 문제가 있습니다.
class DatabaseController {
private let networkRequest: NetworkRequest
init(network: NetworkRequest) {
self.networkRequest = network
}
func connectDatabase() {
networkRequest.connect()
}
}
class NetworkRequest {
func connect() {
// connect to the database
}
}
- 위의 코드를 프로토콜을 이용해 고쳐주면, 아래와 같게 됩니다.
protocol Database {
func connect()
}
class DatabaseController {
private let database: Database
init(db: Database) {
self.database = db
}
func connectDatabase() {
database.connect()
}
}
class NetworkRequest: Database {
func connect() {
// Connect to the database
}
}
- 이로서 구체적인 것이 아닌, 추상화 된 것에 의존할 수 있게 됩니다.
5줄 요약
Quick Summary
In your own journey to become a better engineer, keep in mind the following SOLID principles.
- 하나의 클래스는 하나의 책임만을 갖습니다.
- 클래스는 확장에는 열려있어야 하지만, 수정에는 닫혀있어야 합니다.
- 자식 클래스는 부모 클래스의 정의를 깨뜨려서는 안됩니다.
- 필요한 기능만을 적용해야합니다.
- 추상화에 의존하고, 구체적인 것에 의존해서는 안됩니다.
[참고링크]
https://betterprogramming.pub/an-ios-engineers-perspective-on-solid-principles-bf46ddc25d47
https://www.nextree.co.kr/p6960/
https://dongminyoon.tistory.com/49
'swift' 카테고리의 다른 글
[swift] Delegate와 UML (0) | 2022.09.20 |
---|