SOLID 원칙 초간단 정리

OQ·2022년 2월 27일
0

자료구조

목록 보기
1/2

SOLID

Single Responsiblity Principle (단일 책임 원칙)

소프트웨어의 설계 부품(클래스, 함수 등)은 단 하나의 책임만을 가져야 한다.
= 클래스의 수정 이유는 단 하나여야한다.

나쁜 예)

class LoginService {
    func login(id: String, pw: String) {
        let userData = requestLogin()
        let user = decodeUserInform(data: userData)
        saveUserOnDatabase(user: user)
    }

    private func requestLogin() -> Data {
        // Call API
        return Data()
    }

    private func decodeUserInform(data: Data) -> User {
        // Decoding User Inform from Data
        return User(name: "", age: 10)
    }

    private func saveUserOnDatabase(user: User) {
        // Save User
    }
}

좋은 예)

protocol APIHandlerProtocol {
    func requestLogin() -> Data
}

protocol DecodingHandlerProtocol {
    func decode<T>(from data: Data) -> T
}

protocol DBhandlerProtocol {
    func saveOnDatabase<T>(inform: T)
}

class LoginService {
    let apiHandler: APIHandlerProtocol
    let decodingHandler: DecodingHandlerProtocol
    let dbHandler: DBhandlerProtocol
    
    init(apiHandler: APIHandlerProtocol,
         decodingHandler: DecodingHandlerProtocol,
         dbHandler: DBhandlerProtocol) {
        self.apiHandler = apiHandler
        self.decodingHandler = decodingHandler
        self.dbHandler = dbHandler
    }
    
    func login() {
        let loginData = apiHandler.requestLogin()
        let user: User = decodingHandler.decode(from: loginData)
        dbHandler.saveOnDatabase(inform: user)
    }
}

LoginService라는 클래스에서 API 요청, Data 디코딩, DB 저장 등의 책임을 분배함으로서 결합도는 줄이고 응집도는 높일 수 있게 하였습니다.
이제는 API, Data 값, 저장 방법 등이 수정되더라도 LoginService는 수정되지 않습니다!

Open-Closed Principle (개방-패쇄 원칙)

기존의 코드를 변경하지 않고(Closed) 기능을 수정하거나 추가할 수 있도록(Open) 설계해야 한다.
요구사항이 변경되거나 추가 되더라도 기존 코드는 수정이 일어나지 말아야하고 쉽게 확장하고 재사용 될 수 있어야한다.

나쁜 예)

class Dog {
    func makeSound() {
        print("멍멍")
    }
}

class Cat {
    func makeSound() {
        print("야옹")
    }
}

class Zoo {
    var dogs: [Dog] = [Dog(), Dog(), Dog()]
    var cats: [Cat] = [Cat(), Cat(), Cat()]
    
    func makeAllSounds() {
        dogs.forEach {
            $0.makeSound()
        }
        
        cats.forEach {
            $0.makeSound()
        }
    }
}

좋은 예)

protocol Animal {
    func makeSound()
}

class Dog: Animal {
    func makeSound() {
        print("멍멍")
    }
}

class Cat: Animal {
    func makeSound() {
        print("야옹")
    }
}

class Zoo {
    var animals: [Animal] = []
    
    func makeAllSounds() {
        animals.forEach {
            $0.makeSound()
        }
    }
}

기획자가 Tiger도 추가해주세요 라는 요청을해도 Zoo 클래스는 수정할 필요없고 쉽게 확장하여 사용할 수 있습니다!

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

자식 클래스는 부모클래스에서 가능한 행위를 수행할 수 있어야 한다.
즉 자식 클래스를 사용 중일때, 거기에 부모 클래스로 치환하여도 문제가 없어야 한다.
예를들어 정사각형은 직사각형의 자식이고 넓이를 가져오는 메서드를 수행한다고 했을 때, 정사각형에서 하든 직사각형으로 치환해서 하든 문제 없어야 한다.

나쁜 예)

class Rectangle {	// 직사각형 클래스
    var width: Float = 0
    var height: Float = 0
    
    var area: Float {
        return width * height
    }
}

class Square: Rectangle {   // 직사각형 클래스를 억지로 정사각형 방식으로 사용할 수 있게 변경
    override var height: Float {
        didSet {
            width = height
        }
    }
}

func printArea(of rect: Rectangle) {
	rect.height = 3
	rect.width = 6
	print(rect.area)
}

var square = Square()
printArea(of: square)	// 9

var rectangle = Rectangle()
printArea(of: rectangle)	// 18

// width, height에 같은 값을 넣어도 부모인 Rectangle과 다른 결과가 나오게 됨(LSP 위반)
// 이 버그를 또 억지로 해결한다해도 정사각형에 height, width를 따로 대입해야하는 것도 문제고
// 여러모러 LSP에 위반되는 행위는 차후 유지보수에 계속 문제가 발생하기 쉽습니다.

좋은 예)

protocol Shape {
    var area: Float { get }
}

class Rectangle: Shape {
    var width: Float = 0
    var height: Float = 0
    
    var area: Float {
        return width * height
    }
}

class Square: Shape {
    var length: Float = 0
    
    var area: Float {
        return length * length
    }
}

func printArea(of rect: Shape) {
	print(rect.area)
}

var square = Square()
square.length = 3
printArea(of: square)	// 9

var rectangle = Rectangle()
rectangle.height = 3
rectangle.width = 3
printArea(of: rectangle)	// 9

LSP를 따르게 바꾼 방식에서는 나쁜 예에서 봤던 이슈가 나오는걸 원천봉쇄 함으로서 앞으로 문제가 발생할 여지를 줄여줌

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

객체는 자식이 호출하지 않는 메소드에 의존하지 않아야한다.
하나의 일반적인 인터페이스(swift에서는 프로토콜)보다는, 여러 개의 구체적인 인터페이스가 낫다.
보통 규모가 큰 객체를 상속했을 때 해당 원칙을 위반하기 쉽다.

나쁜 예)

protocol AProtocol {
    func aFunc01()
    func aFunc02()
}

class BClass: AProtocol {
    func aFunc01() {
        print("BClass에서는 aFunc01을 지원하지 않습니다.")
    }
    
    func aFunc02() {
        // 기능 수행
    }
}

좋은 예)

protocol AProtocol01 {
    func aFunc01()
}

protocol AProtocol02 {
    func aFunc02()
}

class BClass: AProtocol02 {    
    func aFunc02() {
        // 기능 수행
    }
}

생각보다 실무에서 많이 발생하는 문제입니다.
SRP를 따른다면 ISP도 자연스럽게 해결될겁니다.

Dependency Inversion Principle (의존 역전 원칙)

의존 관계를 맺을 때, 상위 모듈이 하위 모듈에 의존하면 안된다는 법칙. (추상화 된것에 의존해야함)
예를들어 비지니스 로직은 DB나 뷰같은 구체적인 세부사항에 의존하지 않아야한다.

나쁜 예)

class RestApiService {
    func request() -> String {
        return "success"
    }
}

class LoginService {
    let apiService = RestApiService()	// LoginService는 RestApiService에 의존해버렸다.
    
    func login() {
        let result = apiService.request()
        print(result)
    }
}

나쁜 예에서 추후에 RestApiService 안쓰고 GraphQLApiService 사용하겠다고 하면
LoginService까지 수정해야합니다.

좋은 예)

protocol ApiService {
	func request() -> String
}

class RestApiService: ApiService {
    func request() -> String {
        return "success with Rest Api"
    }
}

class GraphQLApiService: ApiService {
    func request() -> String {
        return "success with gql Api"
    }
}

class LoginService {
    let apiService: ApiService
    
    init(apiService: ApiService) {
        self.apiService = apiService
    }
    
    func login() {
        let result = apiService.request()
        print(result)
    }
}

let loginService01 = LoginService(apiService: RestApiService())
loginService01.login()	// "success with Rest Api"

let loginService02 = LoginService(apiService: GraphQLApiService())
loginService02.login()	// "success with gql Api"

추후에 ApiService를 어떻게 확장하든 LoginService는 수정될 일이 없어진다.

profile
덕업일치 iOS 개발자

0개의 댓글