-
[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