Design Principles

Eli·2021년 2월 15일
1

Clean Architecture

목록 보기
3/5

이전 프로그래밍 패러다임에서는 코드들을 표현화하는 것을 다루었다면

이번 디자인 원칙에서는 그보다 한단계 더 상위 단위인 개념인 클래스 그리고 함수들의 중간 단계에 대한 구조들의 원칙들을 기술한다.

목표는 아래와 같다. (좋은건 다 있는 느낌이랄까...)

  1. 변경에 유연함
  2. 재사용성
  3. 코드의 가독성

Single Responsibility Principle

해당 원칙이 생겨나게 된 배경이나 필요성을 가지고 원칙을 설명하도록 한다.

먼저 위의 사진을 보면 Employee라는 Class를 사용하는 관계자들이 3명이 있다는 부분을 집중하면 된다.

각각 한가지씩의 기능을 필요로 하는데, 그게 모두 한 클래스에 집중이 되어있다는 것이다.

사실 수정이 없다면 더 이상 고민할 필요가 없겠지만, 우리의 소프트웨어는 soft 해야하지 않은가.

그럼 이제 저렇게 구현이 된 상태에서 새로운 기능을 추가한다고 가정해보자.

  1. CFO가 급여를 계산하는데 인센티브 제도를 도입해 Employee 클래스를 수정한다.
  2. COO 역시도 report 함수를 수정할 일이 생겨 수정한다.

동시에 한 클래스를 두명이서 수정하고 있지 않은가.

당연히 그 둘은 이것이 동시에 수정되고 있음도 알 수 없다.

나중에 VCS에서 병합 시점에 알 수 있겠지. 문제는 이 시점에서 발생하게 된다.

병합을 시도하는 과정에서 문제가 발생하면 그것은 또 누가 수정하고 해결을 할 것인가?

저자는 이 부분이 문제라고 이야기한다.

그러므로 class를 분리하여, 두 사람이 동시에 class를 수정하는 일이 없도록 CFO는 CFO가 필요한 파일만 수정할 수 있고. CTO는 CTO가 필요한 파일만 수정해

서로 충돌하는 것을 방지하는 방식으로 해결을 한다고 한다.

SRP 원칙이 지켜진 구조는 아래와 같다.

간단하게 설명하면 Employee Facade class에는 코드가 없으며 하위에 3개의 클래스를 생성해 그들에게 위임하게 된다.

각 참여자들은 해당 클래스를 수정하면 되니, 병합 중 충돌에 대해 걱정하지 않아도 되며, 수정되는 것을 꼭 알아야 할 필요도 없다고 생각한다.

정리해보면 "SRP 원칙은 하나의 모듈을 한 단위의 참여자가 수정/접근하도록 한다" 정도로 말할 수 있을 것 같다.

Open Closed Principle

아래의 코드를 예시로 문제점을 생각해 보자.

struct Book { 
	let id: String 
	let title: String 
	let author: String 
	let rentalPrice: Int 
} 

struct Video { 
	let id: String 
	let title: String 
	let director: String 
	let rentalPrice: Int 
} 

//해당 클래스는 렌탈 가격을 계산해주는 기능을 함수이다.
class RentalPriceCalculator {
 var totalRentalPrice: Int { 
		var result = 0 
		result += self.books.reduce(0, { $0 + $1.rentalPrice })
		result += self.videos.reduce(0, { $0 + $1.rentalPrice }) 
		return result 
	} 

	func addBook(_ book: Book) { 
		self.books.append(book) 
	} 

	func addVideo(_ video: Video) { 
		self.videos.append(video) 
	} 

	private var books = [Book]() 
	private var videos = [Video]() 
}

만약 이곳에서 보드게임을 추가하고 싶은 경우를 생각해보면

RentalPriceCalculator 클래스를 다시한번 손을 봐야한다.

저자는 상위의 모듈을 수정하거나 변경하는 일이 위험한 일이라고 이야기한다.

하위 모듈의 변화로 상위 모듈까지 변경되는 것은 위험하다.

그러므로 하위모듈에 대해선 확장이 가능하지만(open) 상위모듈에 대해서는 변경을 하지 않아도 되도록 하는 것.

이것이 OCP의 개념이다.

그럼 OCP가 지켜진 아래의 코드를 보자.

protocol RentalItem { 
	var id: String { get } 
	var rentalPrice: Int { get } 
}

class RentalPriceCalculator { 
	var totalRentalPrice: Int { 
		return self.items.reduce(0, { $0 + $1.rentalPrice }) 
	} 

	func addRentalItem(_ item: RentalItem) {
		 self.items.append(item) 
	}

	private var items = [RentalItem]() 
}

단순히 프로토콜을 따르는 구조체를 만들어 렌탈 아이템이 추가되더라도 RentalPriceCalculator를 수정하는 일은 없게된다.

그러므로 OCP가 잘 지켜졌다고 볼 수 있다.

Liskov Subsitution Principle

위와 같은 문제를 해결하기 위해 LSP 원칙이 필요한 이유인데 이를 한번 설명해보겠다.

Square는 Rectangle의 하위타입으로 적합하지 않다.

Rectangle에서 넓이 높이는 독립적으로 변경되어야한다. 하지만 Square의 넓이 높이는 반드시 같아야만하므로 항상 함께 변경되어야한다.

그러므로 LSP에 따르면 위와 같은 관계는 옳지 않다

위의 구조를 유지하면서 문제를 해결하기 위해서는 if문으로 타입을 확인해 그에 따른 함수를 호출해야하는데, 평행사변형등의 또 타입이 늘어나면 어떻게 대응하겠는가?

위와 같은 문제들이 발생하지 않게 해결한다고 하면

Shape → Square

   → Rectangle

위와 같이 수직적 확장보단 수평적 방향으로 해결해야하지 않을까 싶다.

저자는 이것의 해결방법으로 항상 상위 하위가 치환이 가능한 상태가 지켜져야 한다고한다.

Interface Segregation Principle

인터페이스를 따르는 객체가 나에게 필요없는 객체를 따르게 됐을 때 생기는 문제의 예시를 보자.

protocol MediaProtocol {
		var isMute: Bool { get set }

		func play()
		func load()
}

//여기서는 별 문제없이 모든 인터페이스 기능을 잘 사용하고 있다.
class OurVideoView: MediaProtocol {
	var isMute: Bool {
		get { return self.player.isMuted }
		set { self.player.isMuted = newValue }
	}

	func play() {
			//play action
	}

	func load() {
			//load something
	}
}

//아래의 예시에서는 슬슬 모든 기능을 사용하지 않고 형식에 맞추기 위해 인터페이스를 사용하는데....
class YoutubeVideoView: MediaProtocol {
	var isMute = false //미사용
	
	func load() {
		//load action
	}

	func play() {
		//미사용
	}
}

//나중에 추가기능으로 음소거일 경우 재생을 하지 않도록하는 기능을 추가
func showMedia(_ view: MediaProtocol) {
		guard !view.isMute else { return }
		view.load()
}
//드디어 문제가 터졌다 우리는 YoutubeViewView는 무조건 isMute를 false로 구현해놓지  않았는가.
//YoutubeVideoView는 절대 재생이 안되는 문제가 생긴다.

Dependency Inversion Principle

의존성 역전의 법칙

사실 SOLID 개념 중에 가장 핵심적인 개념이라 볼 수가 있다.

의존성을 반대방향으로 역전이라는 말로 이해하니 처음에는 쉽게 이해할 수 없었다.

그 의미보다는 추상화된 인터페이스를 통해 직접적으로 의존하던 관계를 간접적으로 변화시킨다는 생각을 가진다면 더욱 쉽게 이해할 수 있을 것 같다.

그럼 DIP로 해결하고자 하는 문제를 살펴보자.

이는 우리가 쉽게 생각할 수 있는 관계이다.

직접적으로 Layer 별로 관계를 맺고 있고 강하게 묶여있다.

그러므로 최하위에 있는 Utility Layer를 변경하게 되면 Policy Layer까지 변경 영향을 미칠 수 있게되며,

여러 부분의 수정에 따른 위험 감수를 따르게 된다.

이제 DIP를 잘 따르는 구조를 한번 보자.

하위 레이어와 상위레이어는 중간에 인터페이스란 추상화된 것에 의존성을 바라보게 된다.

하위 레이어인 Mechanism Layer는 Policy Service Interface만 잘 따르게 된다면 무수히 많은 형태로 변형이 생기더라도 영향이 갈 일이 드물다.

저자는 DIP를 무조건적으로 지키는건 사실 어려운 일이라고 말한다.

그렇지만 머릿속에서 위와 같은 배경과 솔루션을 가질 수 있다는 것을 알고 구조를 잡는 것과 그렇지 못한 것은 큰 차이가 있을 것이다.

실제 DIP를 지키기 위한 저자의 가이드를 보자.

  1. 변할 수 있는 클래스를 참조하지 말아라.
  2. 변할 수 있는 클래스를 따르지 말아라.
  3. 해당 클래스의 함수를 오버라이딩 하지 말아라
  4. 변동성이 큰 것에 대해서 언급 자체를 말아라...

전체적인 규칙을 보면 이야기하고자 하는 것은 변동성이 큰 클래스와 아닌 클래스를 분리하며, 그 클래스들과의 관계는 직접적인 상속, 선언보단 추상화를 통한 인터페이스를 활용해 접근하라는 의미인 것 같다.

참조한 레퍼들

SOLID 원칙을 Swift 코드로 이해해보기

Spring 예제로 보는 SOLID DIP - Yun Blog | 기술 블로그

Dependency inversion principle

profile
애플을 좋아한다. 그래서 iOS 개발을 한다. @Kurly

0개의 댓글