객체지향 프로그래밍 (SOLID)

marisol👩🏻‍💻·2022년 6월 30일
0

객체 지향 프로그래밍(OOP)이라는건 뭘까?
위키백과를 보면 아래와 같이 정의되어 있다

객체지향 프로그래밍은 컴퓨터 프로그램을 명령어의 목록으로 보는 시각에서 벗어나
여러개의 독립된 단위, 즉 객체들의 모임으로 파악하고자 하는 컴퓨터 프로그래밍의 패러다임 중 하나이다.

객체지향의 사실과 오해라는 책에서는 "객체지향이란 시스템을 상호작용하는 자율적인 객체들의 공동체로 바라보고 객체를 이용해 시스템을 분할하는 방법이다"라고 정의하고 있다

SOLID 원칙은 이러한 객체지향 프로그래밍 및 설계의 5가지 기본 원칙을 나타낸 것이며,
객체지향 생활 체조 원칙도 객체지향적인 코드를 작성하기 위한 규칙을 설명하고 있다


📝 SOLID 원칙

SOLID 원칙은 로버트 마틴이라는 사람이 명명한 객체 지향 프로그래밍 및 설계의 5가지 기본 원칙이며,
시간이 지나도 유지 보수와 확장이 쉬운 시스템을 만들고자 할 때 이 원칙들을 적용할 수 있다고 한다

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

: 한 클래스는 하나의 책임만 가져야 한다

하나의 클래스가 모든 책임을 갖고 있는 것이 아니라, 하위 클래스들에게 책임을 넘겨주면 테스트도 용이해지고 기능도 분리할 수 있다

아래 스타벅스 클래스는 주문을 받고, 음료를 제조하고, 고객을 호출하는 여러 책임을 갖고 있다

class Starbucks {
	func serveCustomer() {
    	let menu: Beverage = takeAnOrder()
        
        makeBeverage(menu)
        callCustomer()
    }
    
    func takeAnOrder() -> Beverage {
    	// 고객에게 주문 받음
    }
    
    func makeBeverage(_ beverage: Beverage) {
    	// 음료 제조
    }
    
    func callCustomer() {
    	// 고객 호출
    }
}

단일 책임 원칙을 지켜주기 위해서 책임을 분리해주었다

class Starbucks {
	var server = Server()
    var baristar = Baristar()
    var partTimer = PartTimer()
    
    init(server: Server, baristar: Baristar, partTimer: PartTimer) {
    	self.server = server
        self.baristar = baristar
        self.partTimer = partTimer
    }
    
    func serveCustomer {
    	let menu = server.takeAnOrder()
        
        baristar.makeBeverage(menu)
        partTimer.callCustomer()
    }
}

class Server {
	func takeAnOrder() -> Beverage() {
    	// 고객에게 주문 받음
    }
}

class Baristar {
	func makeBeverage(_ beverage: Beverage) {
    	// 음료 제조
    }
}

class PartTimer {
	func callCustomer() {
    	// 고객 호출
    }
}

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

: 소프트웨어 개체 (클래스, 모듈, 함수 등)는 확장에 대해 열려 있어야 하고, 수정에 대해서는 닫혀 있어야 한다

하나의 개체를 수정할 때, 그 개체를 이용하는 다른 개체들을 줄줄이 고쳐야 한다면 이런 프로그램은 수정하기가 어렵다
개방-폐쇄 원칙이 잘 적용되면, 기능을 추가하거나 변경해야 할 때 이미 제대로 동작하고 있던 원래 코드를 변경하지 않아도, 기존의 코드에 새로운 코드를 추가함으로써 기능의 추가나 변경이 가능하다

개방-폐쇄 원칙은 추상화를 통해 지킬 수 있다

class Shape {
	func drawStar() {
    	let star = Star(numberOfLine: 5, color: "yellow")
        star.draw()
    }
}

class Star {
	let numberOfLine: Int
    let color: String
    
    init(numberOfLine: Int, color: String) {
    	self.numberOfLine = numberOfLine
        self.color = color
    }
}

이 상황에서 별말고 하트를 그려보고 싶다면, 다음과 같이 수정해야 한다

class Shape {
	func drawStar() {
    	let star = Star(numberOfLine: 5, color: "yellow")
        star.draw()
    }
    
    func drawHeart() {
    	let heart = Heart(numberOfHearts: 3, color: "red")
        heart.draw()
    }
}

class Star {
	let numberOfLine: Int
    let color: String
    
    init(numberOfLine: Int, color: String) {
    	self.numberOfLine = numberOfLine
        self.color = color
    }
    
    func draw() {
    	print("I draw a \(color) star with \(numberOfLine) lines")
    }
}

class Heart {
	let numberOfHearts: Int
    let color: String
    
    init(numberOfHearts: Int, color: String) {
    	self.numberOfHearts = numberOfHearts
        self.color = color
    }
    
    func draw() {
    	print("I draw \(numberOfHearts) \(color) hearts")
    }
}

새로운 클래스 사용으로 인해 Shape라는 클래스 안에 메서드가 하나 더 생겼고, 반복되는 모습이다
이를 해결하기 위해 star와 heart를 Drawable이라는 프로토콜을 채택하도록 수정해보면 아래와 같이 바뀐다

protocol Drawable {
	func draw()
}

class Shape {
	func drawShape() {
    	let shapes: [Drawable] = [
    		Star(numberOfLine: 5, color: "yellow"),
        	Heart(numberOfHearts: 3, color: "red")
    	]
    
	    shapes.forEach { shape in
    		shape.draw()
    	}
    }
}

class Star: Drawable {
	let numberOfLine: Int
    let color: String
    
    init(numberOfLine: Int, color: String) {
    	self.numberOfLine = numberOfLine
        self.color = color
    }
    
    func draw() {
    	print("I draw a \(color) star with \(numberOfLine) lines")
    }
}

class Heart: Drawable {
	let numberOfHearts: Int
    let color: String
    
    init(numberOfHearts: Int, color: String) {
    	self.numberOfHearts = numberOfHearts
        self.color = color
    }
    
    func draw() {
    	print("I draw \(numberOfHearts) \(color) hearts")
    }
}

이 다음에 Diamond나 Square 등 다른 Shape이 생기더라도, Drawable을 채택하고 draw()를 정의해주면 된다
=> 확장에 열려있고, 수정에는 닫혀있다

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

: 자료형 S가 자료형 T의 하위형이라면, 프로그램 속성의 변경 없이 자료형 T의 객체를 자료형 S의 객체로 교체(치환)할 수 있어야 한다

리스코프 치환 원칙을 얘기할 때 꼭 나오는 것이 직사각형 클래스로부터 정사각형 클래스를 파생하는 경우이다
직사각형 클래스(T: 상위클래스)로부터 정사각형 클래스(S: 하위클래스)가 파생되었을때, 직사각형을 정사각형으로 치환해도 문제가 없어야 한다는 뜻인데, 너비를 구하는 예시를 보면 LSP를 어기게 된다

<참신러닝 블로그 참고>

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

class Square: Rectangle {
	override var width: Float {
    	didSet {
        	length = width
        }
    }
}

이렇게 되면 직사각형과 정사각형 인스턴스의 넓이가 다르게 나오게 된다
width가 2, length가 5인 경우,
직사각형의 넓이 = 10
정사각형의 넓이 = 4

하위클래스인 정사각형으로는 상위클래스인 직사각형의 넓이를 구할 수 없기 때문에 LSP 원칙 위반이다

그래서 이것도 Protocol로 해결해준다

protocol Polygon {
	var area: Foat { get }
}

class Rectangle: Polygon {
	private let width: Float
    private let length: Float
    
    init(width: Float, length: Float) {
    	self.width = width
        self.length = length
    }
    
    var area: Float {
    	return width * length
    }
}

class Square: Polygon {
	private let side: Float
    
    init(side: Float) {
    	return pow(side, 2) // 제곱함수
    }
}

func printArea(of polygon: Polygon) { // Polygon 프로토콜을 채택하는 모든 타입이 들어올 수 있음
	print(polygon.area)
}

let rectangle = Rectangle(width: 2, length: 5)
printArea(of: rectangle) // 10

let square = Square(side: 2)
printArea(of: square) // 4

Polygon이라는 프로토콜을 직사각형과 정사각형이 각각 채택해주고, 너비를 구하는 메서드를 각각 구현해주었다

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

: 클라이언트는 자신이 이용하지 않는 메서드에 의존하지 않아야 한다

이 원칙은 큰 덩어리의 인터페이스들을 구체적이고 작은 단위들로 분리시킴으로써 클라이언트들이 꼭 필요한 메서드들만 이용할 수 있게 한다

메뉴, 테이크아웃 여부, 손님 번호 정보를 가지고 쥬스를 만드는 JuiceMaker 클래스가 있다

class JuiceMaker {
	var menu: String = "StrawberryJuice"
    var isToGo: Bool = false
    var customerNumber: Int = 33
    var orderedAt: Date = Date()
}

func makeJuice(juiceMaker: JuiceMaker) {
	// 주문 메뉴 확인
    // 테이크아웃인지 매장식사인지 확인
    // customerNumber로 진동벨 호출
}

여기서 juiceMaker의 주문 시간(orderedAt)은 쥬스를 만들 때 필요하지 않은 인터페이스이기 때문에,
Protocol을 정의해서 쥬스를 만드는 makeJuice 메서드가 필요한 정보만 전달해줄 수 있다

protocol JuiceMakable {
	var menu: String { get }
    var isToGo: Bool { get }
    var customerNumber: Int { get }
}

class JuiceMaker: JuiceMakable {
	var menu: String = "StrawberryJuice"
    var isToGo: Bool = false
    var customerNumber: Int = 33
    var orderedAt: Date = Date()
}

func makeJuice(juiceMaker: JuiceMakable) { //
	// 주문 메뉴 확인
    // 테이크아웃인지 매장식사인지 확인
    // customerNumber로 진동벨 호출
}

📌 5. DIP (Dependency Inversion Principle) 의존관계 역전 원칙

: 상위 계층이 하위 계층에 의존하는 의존관계를 역전시킴으로써 상위 계층이 하위 계층의 구현으로부터 독립되게 할 수 있다
이 원칙은 다음과 같은 내용을 담고 있다
1️⃣ 상위 모듈은 하위 모듈에 의존해서는 안된다. 상위 모듈과 하위 모듈 모두 추상화에 의존해야 한다
2️⃣ 추상화는 세부 사항에 의존해서는 안된다. 세부사항이 추상화에 의존해야 한다

예를 들어 은행이 있고, 그 은행이 은행창구매니저를 인스턴스로 갖고 있고 고객의 업무를 처리하는 역할을 맡긴다고 해보면

class Bank {
	let bankManager = BankManager()
    
    func makeWork(customer: Customer) {
    	bankManager.work(for: Customer)
    }
}

class BankManager {
	func work(for customer: Customer) {
    	// 고객 응대하고 업무 처리
    }
}

Bank라는 상위 계층이 BankManager라는 하위 계층에 의존하고 있기 때문에
프로토콜을 생성하고, Bank가 프로토콜에 의존하도록 바꿔주었다

protocol Workable {
	func work(for customer: Customer)
}

class Bank {
	let worker: Workable
    
    init(worker: Workable) {
    	self.worker = worker
    }
    
    func makeWork(customer: Customer) {
    	worker.work(for: Customer)
    }
}

class BankManager: Workable {
	func work(for customer: Customer) {
    	// 고객 응대하고 업무 처리
    }
}

대부분의 원칙을 Protocol을 이용해서 지킬 수 있는 것 같다
객체지향 생활 체조 원칙은 다음 포스팅에서,,🧐


참고 자료

0개의 댓글