요즘 클린아키텍쳐 공부에 대한 필요를 느껴 객체지향, 클린아키텍쳐에 대해 공부를 이어나가고 있다.
오늘은 클린 아키텍쳐 전부에 대해서 설명하기보단 Domain Layer 부분 중 Repository Interface(protocol)을 중점적으로 다룰 예정이다.
그래도 기본적인 공부와 함께..!
그럼 클린 아키텍처를 공부하기 전에
아키텍쳐란 무엇일까?
아키텍쳐는 "구성과 동작원리, 구성 요소 간의 관계 및 시스템 외부 환경과의 관계를 설명하는 설계도"라고 할 수 있다.
그렇다면 아키텍처에 대해서 사람들은 왜 이렇게 중요하다고 할까?
"클린 아키텍처"라는 책에서는 "기능"과 "아키텍쳐"에 두 가지에 대해서 도입부에 이야기를 한다.
지금까지의 나의 개발과정을 되돌아본다면 나는 "기능"에 초점을 맞췄다.
'이 기능할 수 있어', '빠르게 UI만들 수 있어', '서버 이렇게 하면 붙일 수 있어' 어쩌면 당장의 결과물들을 위한 공부를 해왔다고 느꼈다.
책에서는
소프트웨어란 말 그대로 "부드러움을 지니도록" 만들어야 한다, 즉 변경하기 쉬워야한다고 말한다.
예를 들어, 당장은 원하는 결과물로 돌아가지만 전혀 수정이 불가능하다면 이 프로그램은 결국 거의 쓸모가 없어질 것이다.
반대로 당장 동작은 하지않지만 변경이 쉬운 프로그램을 만들어 놓는다면, 언제든지 동작할 수 있도록 할 수 있고 유지보수도 쉬울 것이다.!
때문에 언제든 수정이 가능한 소프트웨어를 만드는데 집중해야 한다!
또 나의 이야기로 돌아오자면 직접 기획하고 개발하는 사이드 프로젝트가 아닌 기획자가 있는 프로젝트를 할 경우 언제든 기획이 변경되어 내가 만들어 놓은 결과물을 변경해야 하는 일이 많다.. 하지만 그 때마다 큰 작업, 대공사처럼 느껴지며 하기에 망설였던 경우들이 많아
이렇게 아키텍쳐에 대해 공부를 하고 적용해본다면 과연 유지보수에 더욱 효과적일지가 궁금했다.
그럼 이제 공부의 필요성에 대해 알아봤고 기본적인 클린 아키텍처 개념들은 다른 분들이 더욱 자세히 설명해주셔서 공부하면서 의문점이 들었던 클린 아키텍처 중 의존성 역전에 대해서 집중적으로 다뤄볼 것이다.
의존성 역전 원칙이란?
의존성 역전 원칙에 대해서 먼저 공부해볼 필요가 있을 것 같다.
의존성 역전 원칙은 객체지향 프로그래밍의 SOLID 원칙 중 하나이다.
변동성이 큰 구체적인 요소에 의존하지 않고 추상에 의존해야 한다.
물론 추상 인터페이스에 변경이 새기면 이를 구체화한 구현체들도 따라서 수정해야 한다.
반대로 구체적인 구현체에 변경이 생기더라도 그 구현체가 구현하는 인터페이스는 대다수의 경우 변경될 필요가 없다.
즉, 인터페이스는 구현체보다 변동성이 낮다.
예시로 확인해보자!
먼저 의존성 역전 원칙을 사용하지 않고 직접적으로 의존성을 가지고자 할때
class OrderProcessor {
var database: ServerDatabase // 직접적인 의존성
init(database: ServerDatabase) {
self.database = database
}
func processOrder(order: Order) {
// 주문 처리 로직
database.save(order: order)
}
}
class ServerDatabase {
func save(order: Order) {
print("나와랏: \(order.orderID), \(order.productName)")
}
}
// 사용 예시
let order = Order(orderID: 1, productName: "Laptop")
let serverDatabase = ServerDatabase()
let orderProcessor = OrderProcessor(database: serverDatabase)
orderProcessor.processOrder(order: order)
OrderProcessor 에 직접적으로 database를 ServerDatabase로 만들었다.
일단 여기까지 ㅇㅋ... 이렇게 했을 때 사실 당해보기 전까지 문제를 인식하는 것이 쉽지 않을 수 있다.
이때 만약에 기획자가 "우리 여기 데이터는 앞으로 서버말고 Realm으로 해볼까?"라고 했을 때
어떻게 수정할 수 있을까?
ServerDatabase와 이번에 바뀐 RealmDatabase는 구조는 같지만 교체가 되지 않는다.. 왜냐면 직접적으로
의존하고 있기 때문이다.
그래서 만약 RealmDatabase로 교체하려면 OrderProcessor 클래스 역시 교체해줘야하는 불편함이 존재한다.
현재는 간단한 예시이기 때문에 심각해보이지 않을 수 있지만 이러한 문제들이 쌓이면 변경이 더더욱 어려워질 수 있을 것이다.
이걸 해결할 수 있는 것이 protocol을 활용한 의존성 역전 원칙이다.
이제 protocol을 이용해서 추상화에 의존할 수 있도록 해보겠다.
// 저수준 모듈
protocol Database {
func save(order: Order)
}
class OrderProcessor {
var database: Database // 추상화에 의존
init(database: Database) {
self.database = database
}
func processOrder(order: Order) {
// 주문 처리 로직
database.save(order: order)
}
}
class ServerDatabase: Database {
func save(order: Order) {
print("나와랏: \(order.orderID), \(order.productName)")
}
}
class RealmDatabase: Database {
func save(order: Order) {
print("나와랏: \(order.orderID), \(order.productName)")
}
}
// 사용 예시
let order = Order(orderID: 1, productName: "Laptop")
// database
let serverDatabase = ServerDatabase()
let realmDatabase = RealmDatabase()
let orderProcessor = OrderProcessor(database: realmDatabase)
orderProcessor.processOrder(order: order)
아까와는 다르게 추상화(protocol)에 의존함으로써 프로토콜을 채택한 클래스를 주입시켜줄 수 있게 되었다.
이러한 방식으로 좀 더 유연한 코드를 만들 수 있는 것 같다!
그럼 이제 실제로 클린 아키텍처에서 의존성 역전을 사용하는 부분인 "리포지터리 유즈케이스"에 대해서 알아보자
(NHN 클린아키텍처 영상은 보면서 많은 도움이 되었다! 그치만 필요한 이유에 대한 이유가 명확하다고 느껴지지 않아 공부해보고 싶었다.)
일반적으로 도메인 레이어에는
"엔티티"
"엔티티"의 사용을 나타내는 "유즈케이스"
그리고 "레포지터리 인터페이스"가 있다.
그리고 데이터 레이어의 리포지터리는
실제적으로 "유즈케이스"가 리포지토리를 참조해서 데이터를 조회한다!
그런데 참조하고 있다는 것은 유연성을 떨어뜨리게 한다. (위에서 본 예시와 같이)
때문에 레포지토리 인터페이스를 도메인 레이어에 두어 유즈케이스는 레포지터리 인터페이스를 의존하고
데이터 레이어의 레포지터리 구현체는 레포지터리 인터페이스를 채택하는 방식으로 의존성 역전 관계를 만든다!
이러면 레포지터리를 교체해야할 때 더욱 유연하게 만들 수 있다.
그럼 플레이 그라운드에서 비슷하게 약식으로 예시를 만들어 보겠다.
//MARK: - Domain
struct Market {
let name: String
let location: String
let member: Int
}
protocol MarketUseCaseProtocol {
func execute() -> AnyPublisher<[Market]?, Never>
}
final class MarketUseCase: MarketUseCaseProtocol {
private let marketRepository: MarketRepositoryInterface
init(marketRepository: MarketRepositoryInterface) {
self.marketRepository = marketRepository
}
func execute() -> AnyPublisher<[Market]?, Never> {
self.marketRepository.data()
}
}
protocol MarketRepositoryInterface {
func data() -> AnyPublisher<[Market]?, Never>
}
//MARK: - Data
protocol MarketRemoteProtocol {
func fetch() -> AnyPublisher<[Market]?, Never>
}
final class MarketRepository: MarketRepositoryInterface {
private let service: MarketRemoteProtocol
init(service: MarketRemoteProtocol) {
self.service = service
}
func data() -> AnyPublisher<[Market]?, Never> {
service.fetch()
}
}
RepositoryInterface를 채택한 Repository 구현체로 서로 레이어를 나눠서 유지보수를 좀 더 용이하게 할 수 있다.
물론 당장 프로젝트에 적용해보지 않아서 직접적으로 효과를 느껴보지 않았지만 앞으로 적용하며 장단점을 더 얘기해보도록 하겠다!
reference
https://www.youtube.com/watch?v=4Rk0MR2BVjI&t=983s
https://www.youtube.com/watch?v=g6Tg6_qpIVc
https://www.yes24.com/Product/Goods/77283734
'개발지식' 카테고리의 다른 글
swift) git 공부하기5 github 연결 git remote add, git push (0) | 2022.08.13 |
---|