ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Swift, 패턴] SOLID에 대한 개념
    iOS/기본 원리 2023. 8. 28. 12:22
    반응형

    SOLID는 객체 지향 디자인을 지향하면서 유지보수성, 확장성, 재사용성을 강조하는 원칙들의 약어입니다.

    이 원칙들은 2000년대 초에 Robert C. Martin에 의해 처음으로 제시되었습니다.


    Swift 언어를 사용해 SOLID 원칙을 설명하는 예시를 제시하겠습니다.

     

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

    클래스는 오직 하나의 책임만을 지녀야 합니다. 이 원칙은 클래스가 바뀔 이유가 하나뿐이어야 함을 의미합니다. 이 원칙을 따르면 코드의 유지보수가 쉬워지며, 다른 기능에 대한 부작용 없이 한 기능을 수정할 수 있게 됩니다.

    // 나쁜 예시
    class Employee {
      var name: String
      var salary: Double
      
      // 책임1: store data
      init(name: String, salary: Double) {
        self.name = name
        self.salary = salary
      }
      
      // 책임2: calculate payroll
      func calculatePayroll() -> Double {
    
      }
    }
    
    // 좋은 예시
    class Employee {
        var name: String
        var salary: Double
        
        init(name: String, salary: Double) {
            self.name = name
            self.salary = salary
        }
    }
    
    class PayrollCalculator {
        func calculatePayroll(for employee: Employee) -> Double {
            // 급여 계산 로직
            return employee.salary * 10_000
        }
    }
    
    let john = Employee(name: "John Doe", salary: 200.0)
    let calculator = PayrollCalculator()
    let payroll = calculator.calculatePayroll(for: john)

    SOLID 원칙을 따를 때, 위 예시와 같이 클래스나 인터페이스의 수가 증가할 수 있습니다.

    이는 코드의 구조와 조직을 분명하게 하여 가독성과 유지보수성을 향상시키는 반면, 코드베이스가 커질 때 관리의 복잡성이 증가할 수 있다는 걱정을 가져올 수 있습니다.

     

    그러나 이러한 원칙을 따르는 것이 장기적으로 다음과 같은 이점을 가져다 줍니다.

    모듈화: 클래스나 함수가 잘 분리되면, 코드의 변경이나 확장이 필요할 때 해당 부분만 수정하면 되므로 유지보수가 쉽습니다.

    재사용성: 잘 정의된 책임을 가진 클래스나 모듈은 다른 곳에서 재사용하기가 쉽습니다.

    테스트 용이성: 작은 단위의 클래스나 함수는 단위 테스트하기가 더 쉽습니다.

     

    코드 관리의 복잡성을 줄이기 위해 아래와 같은 접근법을 고려할 수 있습니다.

    적절한 폴더와 패키지 구조: 기능별, 모듈별로 코드를 잘 구조화하여 관리합니다.

    문서화: 코드와 관련된 주석 및 문서를 잘 유지하여 코드의 목적과 작동 방식을 명확히 합니다.

    코드 리뷰: 다른 개발자와의 코드 리뷰를 통해 코드의 품질을 지속적으로 높이고, 구조적 문제를 미리 파악하고 수정합니다.

     

     

     

    2. Open/Closed Principle (OCP) - 개방/폐쇄 원칙

    소프트웨어의 구성 요소(클래스, 모듈, 함수 등)는 새로운 기능에는 개방되어야 하며 기존 기능의 수정엔 폐쇄되어야 합니다. 즉, 기존 코드를 변경하지 않고 기능을 추가하거나 변경할 수 있도록 설계해야 합니다.

    protocol Shape {
        func area() -> Double
    }
    
    struct Circle: Shape {
        var radius: Double
        func area() -> Double {
            return .pi * radius * radius
        }
    }
    
    struct Square: Shape {
        var side: Double
        func area() -> Double {
            return side * side
        }
    }
    
    struct AreaCalculator {
        func totalArea(shapes: [Shape]) -> Double {
            return shapes.reduce(0, { $0 + $1.area() })
        }
    }

    Shape 프로토콜: 모든 도형은 area() 함수를 구현해야 합니다. 이 프로토콜을 구현함으로써 다양한 도형의 면적을 계산할 수 있게 됩니다.

    Circle Square 도형: Shape 프로토콜을 채택하여 각 도형의 면적을 구하는 로직을 구현하였습니다.

    AreaCalculator: Shape 프로토콜을 따르는 객체의 배열을 받아 그들의 총 면적을 계산합니다.

     

    확장에 열려 있다: 새로운 도형을 추가하려면 Shape 프로토콜을 채택하는 새 구조체나 클래스만 생성하면 됩니다. Triangle 이라는 새로운 도형을 추가하고 싶다면, Shape 프로토콜을 채택하는 Triangle 구조체를 만들면 됩니다.

    수정에 닫혀 있다: 기존의 AreaCalculator나 Shape 프로토콜의 코드는 전혀 바꾸지 않고도 새로운 도형을 추가할 수 있습니다. 즉, 기존의 코드를 수정하지 않아도 확장할 수 있습니다.

     

     

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

    하위 타입은 해당 기본 타입의 대체품으로 사용될 수 있어야 합니다. 이 원칙은 상속과 관련된 원칙으로, 하위 클래스는 상위 클래스의 역할을 교체해서 작동할 수 있어야 합니다.

    // 나쁜 예시
    class Bird {
        func fly() { /*...*/ }
    }
    
    class Duck: Bird {
        override func fly() {
            fatalError("Duck can't fly")
        }
    }
    // Duck는 Bird의 fly()를 제대로 구현하지 못하는 문제가 있다.
    
    // 좋은 예시
    // 모든 새에 공통적인 기능과 속성을 가진 기본 Bird 클래스
    class Bird {
        // Bird의 공통적인 기능 및 속성
    }
    
    // 날 수 있는 새를 위한 프로토콜 정의
    protocol FlyingBird {
        func fly()
    }
    
    // FlyingBird 프로토콜을 채택한 클래스
    class Sparrow: Bird, FlyingBird {
        func fly() {
            // 참새가 날기 위한 로직
        }
    }
    
    // Duck는 Bird만 상속받고, FlyingBird 프로토콜을 채택하지 않음
    class Duck: Bird {
        // 오리에 특정한 기능 및 속성
    }

    나쁜 예시에서, Bird 클래스는 모든 종류의 새를 나타내고 fly() 메서드를 통해 날 수 있다고 가정합니다. 그러나 오리는 날 수 없는 새입니다. 따라서 Duck 클래스에서 fly() 메서드에서 에러 메시지를 발생시킵니다.

    이 문제를 해결하려면 새의 기본 행동을 나타내는 클래스와 인터페이스의 설계를 재검토해야합니다. Bird 클래스에서 fly() 메서드를 제거하고, 날 수 있는 새를 나타내는 별도의 하위 클래스나 프로토콜을 만들어 fly() 메서드를 포함시킬 수 있습니다.

     

     

    4. Interface segregation Principle (ISP) - 인터페이스 분리 원칙

    사용하지 않는 인터페이스의 의존성을 갖도록 강제해서는 안 됩니다. 이는 클라이언트에게 관련 없는 메서드를 구현하는 것을 강요하지 않기 위한 원칙입니다. 즉, 한 클래스는 자신이 사용하지 않는 인터페이스를 구현하도록 강제되어서는 안 됩니다.

    protocol Worker {
        func work()
    }
    
    protocol Eater {
        func eat()
    }
    
    class Human: Worker, Eater {
        func work() { /*...*/ }
        func eat() { /*...*/ }
    }

    Worker 프로토콜: 작업하는 행동을 나타내는 인터페이스입니다.

    Eater 프로토콜: 먹는 행동을 나타내는 인터페이스입니다.

     

    Human 클래스는 이 두 인터페이스를 모두 구현하므로 사람이 할 수 있는 작업과 먹는 행동을 모두 수행할 수 있습니다.

     

    이렇게 인터페이스를 분리함으로써, Human과 같은 클래스는 필요한 기능만을 구현할 수 있게 됩니다.

    만약 Worker와 Eater의 기능이 하나의 큰 인터페이스로 결합되어 있었다면, 모든 구현체는 두 기능을 모두 구현해야 했을 것입니다. 그러나 이런 방식으로 인터페이스를 분리하면, 예를 들어 먹는 기능만 필요한 동물 클래스나 작업만 할 수 있는 기계 클래스를 만들 수 있습니다.

     

     

    5. Dependency Inversion Principle (DIP) - 의존 역전 원칙

    고차원의 모듈은 저차원의 모듈에 의존해서는 안 되며, 둘 다 추상화에 의존해야 합니다. 이 원칙은 구체적인 구현에 의존하기보다는 추상화에 의존하도록 코드를 작성하는 것을 의미합니다. 이를 통해 모듈 간의 유연한 관계를 구축할 수 있습니다.

    protocol Switchable {
        func turnOn()
        func turnOff()
    }
    
    class LightBulb: Switchable {
        private var isOn: Bool = false
        
        func turnOn() { isOn = true; print("LightBulb is turned on."); }
        func turnOff() { isOn = false; print("LightBulb is turned off."); }
    }
    
    class Fan: Switchable {
        private var isRunning: Bool = false
        
        func turnOn() { isRunning = true; print("Fan is running."); }
        func turnOff() { isRunning = false; print("Fan is stopped."); }
    }
    
    class Switch {
        var device: Switchable
        private var isOperating: Bool = false
        
        init(device: Switchable) {
            self.device = device
        }
        
        func operate() {
            if isOperating {
                device.turnOff()
                isOperating = false
            } else {
                device.turnOn()
                isOperating = true
            }
        }
    }
    
    // 사용 예시
    let light = LightBulb()
    let fan = Fan()
    
    let lightSwitch = Switch(device: light)
    lightSwitch.operate()  // LightBulb is turned on.
    lightSwitch.operate()  // LightBulb is turned off.
    
    let fanSwitch = Switch(device: fan)
    fanSwitch.operate()  // Fan is running.
    fanSwitch.operate()  // Fan is stopped.

    Switchable 프로토콜: turnOn 및 turnOff 기능에 대한 추상화를 제공합니다.

    LightBulb 클래스: Switchable 프로토콜을 구현하며 실제 세부 기능을 제공합니다. (저차원 모듈로 볼 수 있습니다.)

    Switch 클래스: Switchable 타입의 디바이스에 의존하며 이 디바이스를 작동시키는 역할을 합니다. (고차원 모듈입니다.)

     

    이 원칙에서 Switch 클래스는 LightBulb에 직접적으로 의존하지 않습니다. 대신, 추상화된 Switchable 프로토콜에 의존합니다. 이로 인해 Switch는 LightBulb만 아니라 다른 Switchable 프로토콜을 구현하는 어떤 객체와도 함께 사용될 수 있게 됩니다.

    반응형

    'iOS > 기본 원리' 카테고리의 다른 글

    [iOS, Swift] Callback 함수와 Closure  (0) 2023.08.30
    [iOS, Swift] Optional 타입  (0) 2023.08.29
Designed by Tistory.