SOLID 원칙이란?

malrang·2022년 3월 20일
0

swift

목록 보기
2/4
post-thumbnail

객체 지향 프로그래밍이란 (OOP)

작은 문제를 해결 할수할 수 있는 객체를 만들고
이 객체들을 조합하여 큰 문제를 해결하게 하는 방식이다.

객체지향 설계를 하면 코드의 재사용, 유지보수 의 용이성 등을 장점으로 가져갈수 있다.

코드는 유연하고 확장할 수 있고 유지보수가 용이하고 재사용할 수 있어야 한다.
이러한 OOP 방식을 준수하기 위해 만들어 진것이 SOLID 원칙이다.

SOLID 원칙

컴퓨터 프로그래밍에서 SOLID 원칙 이란
프로그래머가 시간이 지나도 유지 보수 와 확장이 쉬운 시스템을 만들고자 할 때 이원칙들을 함께 적용할 수 있다.

  • S(SRP)
    • 단일 책임 원칙
    • 한 클래스는 하나의 책임만 가져야 한다.
  • O(OCP)
    • 개방-폐쇄 원칙
    • 소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.
  • L(LSP)
    • 리스코프 치환 원칙
    • 프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.
  • I(ISP)
    • 인터페이스 분리 원칙
    • 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다
  • D(DIP)
    • 의존관계 역전 원칙
    • 프로그래머는 추상화에 의존해야지 구체화에 의존하면 안된다.
    • 의존성 주입은 이 원칙을 따르는 방법 중 하나이다.

SRP(Single-Responsibility Principle) 단일 책임의 원칙

소프트웨어 요소(클래스, 모듈, 함수 등)는 응집도 있는 하나의 책임을 갖는다.
클래스를 변경해야 하는 이유는 단지 응집도여야 한다.

class ShoppingMall {
    
    func ShoppingMallSales() -> Int? {
        let sales = coffeeShop.coffeeShopSales() + clothingStore.clothingStoreSales() +
        restaurant.restaurantSales()
        
        return sales
    }
    
    func coffeeShopSales() -> Int {
        return 0
    }
    
    func clothingStoreSales() -> Int {
        return 0
    }
    
    func restaurantSales() -> Int {
        return 0
    }
}

위의 예시로 보게되면 ShppingMall 에는 여러가지 가게들이 입점 해있는 상태이다
쇼핑몰은 입점해있는 모든 가게의 매출을 더해 집계할수 있는 기능을 가지고 있다.
지금은 예시로서 내부 구현을 하지 않은상태이지만 구현하게된다면 각각 해야하는 일들이 많아지게되고
ShoppingMall 의 Class 또한 덩치가 매우 커지게 된다.

이때 SRP 를 지키도록 하나의 클래스가 하나의 책임을 갖도록 하려면 기능들을 분리해주어야한다.

class ShoppingMall {
    let coffeeShop = CoffeeShop()
    let clothingStore = ClothingStore()
    let restaurant = Restaurant()
    
    func ShoppingMallSales() -> Int? {
        let sales = coffeeShop.coffeeShopSales() + clothingStore.clothingStoreSales() +
        restaurant.restaurantSales()
        
        return sales
    }
}

class CoffeeShop {
    func coffeeShopSales() -> Int {
        return 0
    }
}

class ClothingStore {
    func clothingStoreSales() -> Int {
        return 0
    }
}

class Restaurant {
    func restaurantSales() -> Int {
        return 0
    }
}

이렇게 각각의 책임을 하위 클래스에게 넘겨주게 되면 각각의 클래스를 테스트 하기 쉬워지게 된다.

OCP(Open-Close Principle) 개방-폐쇄 원칙

소프트웨어 요소는 확장 가능 하도록 열려있고, 변경에는 닫혀 있어야 한다.
확장을 할때는 기존의 코드를 최대한 건드리지 않고 확장하자.
새 기능을 추가 할 때 변경하지 말고 새 클래스나 함수를 만들어라.

확장할때는 최대한 기존의 코드를 수정하지 않고 기능을 추가할수있도록 해야한다!

추상화 를 활용해 해결할수 있다.(예를 들면 protocol)

class Fruits {
    let 딸기 = "딸기"
    let 바나나 = "바나나"
}

class FruitStore {
    var 과일들: [Fruits] = [Fruits(), Fruits()]
}

위의 코드를 보면 과일들 이라는 프로퍼티 내부에는 Fruit 타입만 저장될수있다.
지금은 과일의 종류가 딸기, 바나나 2개 밖에 없지만 과일의 종류를 더 추가하기 위해서는 Fruit 내부에 새로운 과일의 프로퍼티를 만들어 주어야한다.
이는 OCP 원칙에 위배되는 행위이다.

이를 해결하고자 procotocol 을 활용해 해결할수 있다.

protocol Fruits {}

class 딸기: Fruits {
    let name = "딸기"
}

class 바나나: Fruits {
    let name = "바나나"
}

class 두리안: Fruits {
    let name = "두리안"
}

class FruitStore {
    var 과일들: [Fruits] = [딸기(), 바나나(), 두리안()]
}

위 처럼 과일들이 Fruits 프로토콜을 채택하는 형태고 만들게되면
과일들 프로퍼티는 [Fruit] 즉 Fruits 프로토콜을 채택하는 녀석이라면 누구든 저장할수 있는 형태가 된다.

1번 예시와 다른점이 있다면 새로운 과일을 추가시킬때 Fruits 프로토콜을 채택만 하면 되기 때문에 기존에 작성해둔 코드를 수정하지 않아도 과일의 종류를 추가시킬수 있게 된다.

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

서브타입은 (상속받은) 기본 타입으로 대체 가능 해야 한다.
자식 클래스는 부모 클래스 동작(의미)를 바꾸지 않는다.

서브클래스(자식클래스)는 부모의 역할을 온전히 다 사용할수 있어야 한다.
부모 클래스의 기능을 제한하면 안된다라고 이해할수 있겠다.

class 의 overriding 을 사용하게되면 리스코프 치환원칙을 위배할수있다.

아래의 예시는 직사각형과 정사각형의 예시이다.
정사각형은 직사각형 에 포함된다 할수있다.
그렇기에 정사각형이 직사각형을 상속 받았을때에 일어나는 일이다.

class Rectangle {
    var width = 10
    var height = 5
}

class Square: Rectangle {
    override var height: Int {
        didSet {
            height = width
        }
    }
}

직사각형의 경우 높이와 길이가 다를수 있지만 정사각형의 경우 길이와 높이가 같아야한다. 이를 해결하기 위해 override 하여 재정의 해주었지만 이러한 경우 부모의 역활을 자식에서 대신하지못하는 상황이 발생된다.

이를 해결하기위해 protocol 을 활용하여 해결할수 있다.

protocol Shape {
    var width: Int { get }
    var height: Int { get }
}

class Rectangle: Shape{
    var width = 10
    var height = 5
}

class Square: Shape {
    var width: Int = 5
    var height: Int = 5
}

위의 예시처럼 Rectangle, Square 모두 Shape(모양) protocol 을 채택하게되면 프로토콜 내부의 프로퍼티 값(구현부)은 해당 프로토콜을 채택하는 하위 클래스에서 구현하도록 하면 LSP 의 원칙에 어긋나지 않는 프로그램을 설계할수 있게 된다.

이때 둘다 공통으로 가지고 있어야할 기능이 있다면 protocol 의 기본구현을 이용해 프로토콜을 채택한 하위 객체는 기본구현된 기능을 사용할수 있게된다.
(마치 상속처럼 사용할수 있게되며, struct 에서도 활용 가능하다 프로토콜 짱짱맨, 이를 이용하면 다중 상속처럼 사용도 가능하다..!)

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

인터페이스를 작게 분리 유지해야 한다.
클라이언트 객체는 사용하지 않는 메소드에 의존하면 안된다.

불필요한 인터페이스 요소들을 포함 시키지 말아라.

protocol 예시

protocol Gesture {
    func click()
    func doubleClick()
}

위와 같은 프로토콜을 만들었다 사용할때에는 위의 프로토콜을 채택하여 사용하면 click, doubleClick 기능을 구현하여 사용할수 있게된다.

하지만 이때 click 기능만 필요하다면 어떤가?
doubleClick 는 깡통으로 만들어둬야하나???

class Button: Gesture {
    func click() {
        print("클릭!")
    }
    
    func doubleClick() {}
}

이러한 경우가 생길수 있기에 해당 기능들을 나누어 구현해주는 방식으로 구성해야한다. 위의 상황은 ISP 를 위반하는 인터페이스 라고 할수 있겠다.

protocol click {
    func click()
}

protocol DoubleClick {
    func doubleClick()
}

class Button: click {
    func click() {
        print("클릭!")
    }
}

이렇게 나누어진 프로토콜중 필요한 프로토콜만 채택하게되면 원하는 기능만 구현하여 사용할수 있게된다.

class 예시

class Malrang {
    let name = "malrnag"
    var age = "29"
    var hairStyle = "장발"
    
    func printProfile(profile: Malrang) {
    }
}

나의 정보를 포함하고 있는 Malrang 이라는 클래스가 있다.
printProfile 메서드는 나의 정보를 출력해주는 기능이다.
이때 사실 나이와 이름 은 알아야할 정보지만 헤어스타일은 굳이..출력하고 싶지않다.
그렇다면 매개변수로 Malrang 타입을 받아 모든 정보를 받아올 필요가 있을까?

이런 상황에서도 protocol 을 활용함으로써 printProfile 가 필요로 하는 정보만 전달해줄수 있도록 할수있다.

protocol Profile {
    var name: String { get }
    var age: Int { get  }
}

class Malrang: Profile {
    let name = "malrnag"
    var age = 29
    var hairStyle = "장발"
    
    func printProfile(profile: Profile) {
    }
}

printProfile 의 매개변수에는 Profile 을 채택하는 것들로만 들어올수 있게 된다.

DIP(Dependency-Inversion Principle) 의존관계 역전 원칙

상위 레벨 모듈은 하위 레벨에 의존하면 안된다.(둘 다 추상화된 인터페이스에 의존해야 한다 추상화는 구체화에 의존하면 안되고, 구체화는 추상화에 의존하면 안된다.)
-ex 프로토콜 델리게이트 패턴 사용 하여 해결가능!

추상화는 위의 예시들에서 본 것 처럼 protocol 을 이용하는것 이것을 추상화에 의존했다 라고 할수 있겠다.

예시를 보도록 하자!
과일가게(FruitStore)class, 과일가게의 사장(JuiceMaker)class 가 있다고 가정해보자.

class FruitStore {
    var fruitList = ["딸기", "바나나", "두리안"]
}

class JuiceMaker {
    let fruitStore = FruitStore()
    
    func printFruitList() {
        print(fruitStore.fruitList)
    }
}

위의 예시는 과일가게 사장이 과일가게의 인스턴스를 내부적으로 생성해 사용하게된다.

여기서 JuiceMaker 는 상위레벨, FruitStore는 하위레벨 인데 printFruitList 메서드를 사용하는 경우 JuiceMaker가 FruitStore를 의존하게 된다.

이러한 상활일때도 추상화 를 통해 상위레벨이 하위레벨을 의존하지 않게 바꿀수 있겠다.

protocol Fruits {
    var fruits: [String] { get }
    func fruitList()
}

class FruitStore: Fruits {
    var fruits = ["딸기", "바나나", "두리안"]
    
    func fruitList() {
        print(fruits)
    }
}

class JuiceMaker {
    let fruitList: Fruits
    
    init(fruitList: Fruits) {
        self.fruitList = fruitList
    }
    
    func printFruitList() {
        fruitList.fruitList()
    }
}

var 과일가게 = FruitStore()
var 과일가게사장 = JuiceMaker(fruitList: 과일가게)
과일가게사장.printFruitList()

위의 예시를 보게 되면 추상화(프로토콜)를 통해 서로가 프로토콜을 의존하도록 수정한예시 이다.

Fruits 라는 프로토콜을 정의하고 프로토콜 내부에 과일들이 저장된 fruits 프로퍼티와 과일들의 목록을 출력하는 기능fruitList 를 채택한 객체에서 구현하도록 하였다.

FruitStore 가 Fruits 프로토콜을 채택하고 내부를 구현해두었다.
JuiceMaker 는 FruitStore 를 소유하는 형태가 아닌 프로토콜을 채택하는 녀석을 저장할수있는 fruitList 라는 프로퍼티를 만들어 이를 활용하는 형태로 변경하였다.
fruitList 는 Fruits 를 채택하는 애들만 저장할수 있기 떄문에
fruitList 는 Fruits 에 정의된 프로퍼티, 기능들을 사용할수 있게된다.

이렇게 fruitList가 FruitStore 를 의존하는 형태가 아닌 둘다 Fruits(프로토콜) 을 의존하는 형태로 수정함으로써 각각의 객체를 테스트할때도 수월해지며 fruitList 사용이 용이해진다.

SOLID 원칙을 지키려 노력하자

SOLID 를 매번 모두 지킬수는 없지만 지키려 노력하게 되면 코드의 질을 향상시키고 재사용 과 유지보수가 수월해져 더 효율적인 코드를 작성할수 있게 된다.

profile
my brain is malrang

0개의 댓글