SOLID 원칙
객체 지향 프로그래밍에서 지켜져야 하는 다섯 가지 원칙을 의미합니다.
S - 단일 책임 원칙 (Single Responsibility Principle)
O - 개방-폐쇄 원칙 (Open-Closed Principle)
L - 리스코프 치환 원칙 (Liskov Substitution Principle)
I - 인터페이스 분리 원칙 (Interface Segregation Principle)
D - 의존 역전 원칙 (Dependency Inversion Principle)
(이 다섯 가지 원칙을 줄여서 SOLID 원칙이라고 합니다. 이를 지키면 객체 지향 프로그래밍에서 유지보수, 확장, 재사용 등을 더 효과적으로 할 수 있습니다.)
단일 책임 원칙
소프트웨어 개발의 SOLID 원칙 중 하나로, 하나의 클래스는 하나의 *책임만 가져야 한다는 원칙입니다. 즉, 클래스가 변경되어야 하는 이유는 오직 하나뿐이어야 한다는 것을 의미합니다. 이를 준수하면 클래스의 응집성(cohesion)이 높아져서 클래스를 유지보수하고 확장하기 쉬워지며, 결합도(coupling)가 낮아져서 다른 클래스와의 의존성을 줄일 수 있습니다. 이는 코드의 재사용성을 높이고 유지보수를 용이하게 하며, 코드의 가독성과 이해도를 높일 수 있습니다.
코드예제
(학생 클래스는 학생 정보를 담당하고, 학생관리자 클래스는 학생 정보의 추가와 조회 등 관리 역할만을 담당합니다. 이렇게 단일책임 원칙을 지켜 작성된 코드는 나중에 수정이 필요할 때 해당 기능만 수정하면 되므로 유지보수성이 높아집니다.)
class 학생 {
var 이름: String
var 학번: String
var 전공: String
init(이름: String, 학번: String, 전공: String) {
self.이름 = 이름
self.학번 = 학번
self.전공 = 전공
}
}
class 학생관리자 {
var 학생들: [학생] = []
func 학생추가(학생: 학생) {
학생들.append(학생)
print("새로운 학생 정보가 추가되었습니다.")
}
func 학생목록() {
if 학생들.count == 0 {
print("등록된 학생 정보가 없습니다.")
} else {
for 학생 in 학생들 {
print("이름: \(학생.이름), 학번: \(학생.학번), 전공: \(학생.전공)")
}
}
}
}
개방-폐쇄 원칙
기존 코드를 변경하지 않고 새로운 기능을 추가할 수 있도록 하는 것을 목적으로 합니다. 이를 위해 모듈, 클래스, 함수 등의 소프트웨어 요소는 확장에는 열려 있어야 하고 변경에는 닫혀 있어야 합니다. 즉, 새로운 기능을 추가하기 위해서는 기존 코드를 수정하는 것이 아니라, 새로운 코드를 추가하거나 기존 코드를 확장하여 구현해야 합니다. 이를 통해 소프트웨어의 유지보수성과 확장성을 높일 수 있습니다.
코드예제
(사각형, 삼각형, 원 클래스는 도형 프로토콜을 채택하고, 면적() 메서드를 구현합니다. 이렇게 함으로써, 새로운 도형 클래스를 추가하더라도 면적계산기 클래스를 변경할 필요 없이 기존 코드를 확장할 수 있습니다.)
protocol 도형 {
func 면적() -> Double
}
class 사각형: 도형 {
var 가로: Double
var 세로: Double
init(가로: Double, 세로: Double) {
self.가로 = 가로
self.세로 = 세로
}
func 면적() -> Double {
return 가로 * 세로
}
}
class 삼각형: 도형 {
var 밑변: Double
var 높이: Double
init(밑변: Double, 높이: Double) {
self.밑변 = 밑변
self.높이 = 높이
}
func 면적() -> Double {
return (밑변 * 높이) / 2
}
}
class 원: 도형 {
var 반지름: Double
init(반지름: Double) {
self.반지름 = 반지름
}
func 면적() -> Double {
return Double.pi * 반지름 * 반지름
}
}
class 면적계산기 {
func 계산(도형: 도형) -> Double {
return 도형.면적()
}
}
리스코프 치환 원칙
하위 타입(subtype)은 상위 타입의 대체 가능해야 한다는 원칙입니다. 이를테면, 어떤 클래스 A가 클래스 B의 상위 타입이라면, 어디서든 클래스 A를 대신해서 클래스 B를 사용할 수 있어야 한다는 것을 의미합니다. 이러한 원칙은 소프트웨어 시스템에서 유연성과 확장성을 향상시키는 데 중요한 역할을 합니다. 따라서 상위 타입과 하위 타입 간의 관계가 명확하게 정의되고, 그 관계가 유지되도록 설계되어야 합니다.
코드예제
(Rectangle 클래스와 Square 클래스를 선언하고, Square 클래스가 Rectangle 클래스를 상속받도록 구현하였습니다.
Square 클래스는 width와 height가 항상 같은 정사각형을 나타내기 때문에, width와 height가 설정될 때 서로 값을 변경하도록 override된 구현을 갖고 있습니다.
여기서 중요한 점은 Square 클래스가 Rectangle 클래스를 대체할 수 있는지 여부입니다.
이 예제에서는 Rectangle 인스턴스와 Square 인스턴스를 모두 Rectangle 타입으로 선언하고 있습니다.
그러나 Square 클래스는 Rectangle 클래스의 멤버 함수들을 모두 상속받아 정상적으로 작동합니다.
따라서 이 예제는 Liskov Substitution Principle을 잘 지키고 있는 예라고 할 수 있습니다.)
// Rectangle 클래스
class Rectangle {
var width: Double
var height: Double
init(width: Double, height: Double) {
self.width = width
self.height = height
}
func area() -> Double {
return width * height
}
}
// Square 클래스
class Square: Rectangle {
override var width: Double {
didSet {
height = width
}
}
override var height: Double {
didSet {
width = height
}
}
}
// 사용 예시
let rectangle: Rectangle = Rectangle(width: 4, height: 5)
let square: Rectangle = Square(width: 4, height: 4)
// area() 함수는 정상적으로 동작합니다.
print("Rectangle area: \(rectangle.area())") // 출력 결과: "Rectangle area: 20.0"
print("Square area: \(square.area())") // 출력 결과: "Square area: 16.0"
인터페이스 분리 원칙
클라이언트가 자신이 사용하지 않는 메서드에 의존하지 않도록 인터페이스를 분리해야 한다는 것을 강조합니다. 즉, 한 인터페이스가 너무 많은 일을 하지 않도록 하고, 클라이언트가 필요로 하는 기능만 제공하는 작은 인터페이스들로 분리해야 한다는 것입니다. 이렇게 하면 코드 유지 보수성, 재사용성, 확장성이 향상되며, 결합도와 응집도도 개선됩니다.
코드예제
(Machine 이라는 인터페이스는 print(), scan(), fax() 의 모든 기능을 가지고 있습니다. 하지만 AllInOneMachine 클래스에서는 이 인터페이스를 구현함으로써 모든 기능을 제공하고 있습니다. 이는 인터페이스 분리 원칙을 위반하는 코드입니다.
그러나 MultiFunctionPrinter, SimplePrinter, SimpleScanner 클래스에서는 각각의 기능에 대해 분리된 인터페이스를 제공하고 있습니다. 이러한 설계는 인터페이스 분리 원칙을 준수하는 예제 코드입니다.)
// Bad Example - 하나의 interface 에서 모든 메서드를 제공하고 있다.
protocol Machine {
func print()
func scan()
func fax()
}
class AllInOneMachine: Machine {
func print() {
// print implementation
}
func scan() {
// scan implementation
}
func fax() {
// fax implementation
}
}
// Good Example - 각각의 기능을 분리된 interface 로 제공하고 있다.
protocol Printer {
func print()
}
protocol Scanner {
func scan()
}
protocol Fax {
func fax()
}
class MultiFunctionPrinter: Printer, Scanner, Fax {
func print() {
// print implementation
}
}
class SimplePrinter: Printer {
func print() {
// print implementation
}
}
class SimpleScanner: Scanner {
func scan() {
// scan implementation
}
}
의존 역전 원칙
고차원의 모듈은 저차원의 모듈에 의존해서는 안 된다는 것을 말합니다. 대신 두 모듈 모두 추상화된 것에 의존해야 합니다. 즉, 모듈 간의 결합도를 낮추기 위해 추상화된 인터페이스에 의존해야 한다는 것입니다. 이렇게 하면 모듈이 변경될 때 다른 모듈에 미치는 영향을 최소화할 수 있습니다. 이러한 방식으로 코드를 구성하면 모듈 간의 의존성을 완화시켜 유연성을 높이고, 유지보수성을 향상시키는 데 도움이 됩니다.
코드예제
(전화기사용자는 전화기 프로토콜에만 의존하며, 구체적인 전화기 구현체(아이폰, 갤럭시)에는 의존하지 않습니다. 따라서 전화기 구현체를 추가하거나 변경해도 전화기사용자 코드에는 영향을 주지 않습니다.)
protocol 전화기 {
func 전화걸기()
}
class 아이폰: 전화기 {
func 전화걸기() {
print("아이폰으로 전화를 걸고 있습니다.")
}
}
class 갤럭시: 전화기 {
func 전화걸기() {
print("갤럭시로 전화를 걸고 있습니다.")
}
}
class 전화기사용자 {
var 전화기: 전화기
init(전화기: 전화기) {
self.전화기 = 전화기
}
func 전화하기() {
전화기.전화걸기()
}
}
let 아이폰사용자 = 전화기사용자(전화기: 아이폰())
아이폰사용자.전화하기()
let 갤럭시사용자 = 전화기사용자(전화기: 갤럭시())
갤럭시사용자.전화하기()
'iOS' 카테고리의 다른 글
Swift XML 파싱 (0) | 2023.05.03 |
---|---|
객체지향의 역할, 책임 차이 쉬운 예 (0) | 2023.05.03 |
iOS UDP 통신 쉬운 예제 코드 (0) | 2023.04.30 |
TCP, UDP (0) | 2023.04.29 |
강한순환참조 (0) | 2023.04.28 |