클린아키텍쳐가 무엇이고, 누가 이 개념을 시작했는지 등 이 개념에 대한 설명은 저보다 더 잘 설명하고 있는 문서가 많으므로 이 부분은 생략하려고 해요. 4일이라는 짧은 시간동안 기존 프로젝트를 클린아키텍쳐의 개념을 이용해 리팩토링해보면서 고민한 부분들, 클린아키텍쳐에 대한 이해를 위해 어떻게 학습했는지 등을 공유해 보겠습니다.
SwiftUI Clean Architecture 를 검색해서 상단에 나오는 글을 무작정 읽어보았어요. 가장 눈에 띄는 단어는 의존성 이였는데요.
의존성 무엇인가 라고 누가 물어보면 어떻게 설명할 수 있을까 고민되더라구요.
우리가 실생활에서 사용하는 의존이란 단어는 너무나 잘 알고 있는 의미잖아요. 그런데 이게 도대체 클린아키텍쳐를 설명하는 글에서 왜 이렇게 많이 나오는 것인가 하는 궁금즘도 생겼구요.
우선 저는 이 개념을 이렇게 설명 할 수 있을 것 같아요.
갓 태어난 애기를 상상해볼께요. 이 아가는 밥 먹는 것도, 화장실 가는 것도, 잠 자는 것도 심지어 트림도 모두 보호자의 도움으로 하게 되죠. 이 아기는 자신을 돌보는 보호자가 없다면 그 무엇도 할 수 없을거에요. 즉 아기는 부모에게 의존 하고 있다고 표현할 수 있겠죠?

저는 프로그래밍에서도 마찬가지라고 이해했습니다.
즉, 객체 A가 객체 B에 의존하고 있다 는 것은 A는 B없이 동작할 수 없는 형태라는 겁니다.
아래 코드로 볼까요?
사실 Parent 타입에서도 Baby타입의 속성을 가져야 실제 세계와 유사하겠지만, 잠시 이 생각은 뒤로 제쳐둡시다! 의존성에 초점을 맞춰볼게요.
struct Baby {
let parent: Parent
let name: String
let age: UInt
func eat() {
self.parent.feed()
}
}
struct Parent {
//MARK: Properties
..
//MARK: Methods
func feed(){
//
}
}
Baby 타입은 parent를 반드시 가져야하며 이 parent가 없다면 존재할 수 없을거에요(인스턴스화 될 수 없다는 의미)
저의 프로젝트에선 이런 코드예시를 가져올 수 있을것 같아요
//MARK: - UseCases
protocol RefreshClimbingRecordUseCase {
func fetch(requestValue: ClimbingRecordDate) async -> Result<MonthRecordEntity, Error>
}
final class RefreshMonthClimbingRecordUseCase: RefreshClimbingRecordUseCase {
private let repository: RecordsRepository
init(repository: RecordsRepository) {
self.repository = repository
}
func fetch(requestValue: ClimbingRecordDate) async -> Result<MonthRecordEntity, Error> {
//MARK: Devlivering info for Networking
let result = await repository.fetch(requestValue: requestValue)
return result.mapError { $0 as Error }
}
}
클린아키텍쳐에서 의존이 많이 언급되는 이유는 그것의 가장 중요한 원칙 중 하나가 바로 각 레이어는 단방향으로 의존한다는 것이기 때문이에요.
다시 말하자면 각 레이어는 자신보다 밖에 있는 레이어를 의존해선 안된다는 이야기입니다. 예를 들어 UseCases는 Controllers, Gateways, Presenters 역할을 모르고 있어야한다는 이야기죠.
여기서 말하는
모르고 있다라는 것은 UseCases 내부에서자신의 레이어 밖에 있는 것들을 properties, method의 매게변수, return 타입 등으로 사용하지 않아야 한다는 이야기 입니다.

그럼 왜 단방향으로 의존하도록 강조하고 있을까요?
조금 슬픈 이야기지만, 단방향 의존은 마치 짝사랑 같아요. 사랑을 받고 있는 사람은 짝사랑하는 사람이 누구인지 모르고, 그 사람이 나를 떠나도, 나를 그만 좋아해도 전혀 영향이 없죠. 꼭 이사람 뿐만 아니라 다른 사람이 나를 짝사랑하게 된다고 해도 나의 삶에는 전혀 지장이 없을 거에요.
단방향 의존도 마찬가지랍니다. UseCases가 변하더라도 Entities는 UseCases가 자신을 짝사랑하고 있는지 알지 못하기 때문에 새로운 UseCasees가 생긴다고 하더라도 그 역할과 동작에는 전혀 문제가 없는 거죠.

갑자기 너무 어려운 단어가 나오죠?
이 개념은 SOLID라는 객체 지향 프로그래밍 및 설계의 다섯 가지 기본 원칙중 하나에요. 클린아키텍쳐에선 이 DIP를 이용해 객체간의 결합도를 낮추고 있답니다. (즉 서로가 서로를 의존하는 관계를 줄이도록 하고 있어요)
다시 Baby, Parent 코드를 생각해볼께요. 의존성 역전이라는 것은 쉽게말하면 Parent외에도 특정 조건만 만족한다면 그 누구든 아기의 부모가 될 수 있도록 한다는거에요.
즉 특정 객체(or 타입, 인스턴스)가 아니라 특정 조건을 만족하는 객체를 의존하도록 하여 의존 방향을 역전하는 행위라고 볼 수 있을것 같아요.
Baby, Parent 코드를 프로토콜을 이용해 의존성을 줄이도록 리팩토링 해볼께요
// 1. 특정조건 만들기 : Parentable 프로토콜 타입 내부에 보호자가 될 수 있는 조건을 명시해요.
protocol Parentable {
func feed()
func putSleep()
}
struct Baby {
// 2. 기존의 Parent가 아닌 Parentable타입으로 변경해요.
let parent: Parentable
let name: String
let age: UInt
func eat() {
self.parent.feed()
}
}
struct Parent: Parentable {
//MARK: Properties
..
//MARK: Methods
func feed(){
//
}
func putSleep() {
}
}
struct GrandParents: Parentable {
//MARK: Properties
..
//MARK: Methods
func feed(){
//
}
func putSleep() {
//
}
}
struct Goverment: Parentable {
//MARK: Properties
..
//MARK: Methods
func feed(){
//
}
func putSleep() {
//
}
}
기존 코드에선 Baby는 무조건 Parent 타입의 인스턴스만 속성으로 가질 수 있었지만, 리팩토링한 구조에선 GrandParents, Goverment 등 Parentable 프로토콜을 채택한 모든 타입의 인스턴스를 parents로 가질 수 있게 돼요.
하는 의문이 드시나요? 저 또한 클린아키텍쳐를 적용하면서 초반에 이 물음에 명확한 답변을 내리기 어려웠는데요!
Entity로 DTO(Data Transfer Object)를 mapping 하면서 제 나름의 답변을 정리할 수 있었어요.
//Entity, DTO 역할을 동시에 수행
struct CalenderModel: Decodable, Identifiable,Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
static func == (lhs: CalenderModel,
rhs: CalenderModel) -> Bool {
return lhs.id == rhs.id
}
let id: Int
let `where`: ClimbingLocation?
let when: String
let level: Int
let score: Float
let picture: [String?]?
let thumbnail: String?
enum CodingKeys: String, CodingKey {
case id
case `where`
case when
case level
case score
case picture
case thumbnail
}
struct ClimbingLocation: Decodable {
let id: Int
let name: String
let address: String
let latitude: Float
let longitude: Float
}
}
//Entity, DTO 역할을 분리
//
// MARK: - Climbing Data Transfer Object (기존에 CalenderModel이였던 것)
struct ClimbingMonthRecord {
let list: [ClimbingRecord]
}
struct ClimbingRecord: Decodable {
let id: Int
let `where`: ClimbingLocation?
let date: String
let level: Int
let score: Float
let picture: [String?]?
let thumbnail: String?
enum CodingKeys: String, CodingKey {
case id
case `where`
case date = "when"
case level
case score
case picture
case thumbnail
}
struct ClimbingLocation: Decodable {
let id: Int
let name: String
let address: String
let latitude: Float
let longitude: Float
}
}
// MARK: - Mappings to Domain (실제 사용하는 데이터와 연결, 기존의 DividedWeekData 타입의 모델로 맵핑 필요)
extension ClimbingMonthRecord {
func transformToDomin() -> MonthRecordEntity {
return .init(monthRecord: self)
}
}
제가 내린 답변은 위와 같아요.
여러분은 어떻게 생각하시나요?
4일 공부하고나서 감히 핵심이라고 이야기할 순 없겠지만, 제가 봤을 때 가장 중요한 것은 의존성관리(단방향 의존, 의존성 줄이기)와 내게 필요한 적절한 레이어 선택 및 그 레이어 대한 정의라고 생각해요.

그리고 더 중요한건, 레이어의 범위는 절대적이지 않다는것입니다.
단방향으로 의존하고 있으며 각각의 레이어와 그 경계에 대한 역할과 정의를 내렸다면 클린아키텍쳐를 잘 적용(?)하고 있는 것이라고 생각해요.
가끔 우리는 우리가 사용하고자하는 디자인패턴, 설계에 더 초점을 맞추고 우리의 모든 서비스내용을 그것에 끼워맞추려고 하는 때가 종종 생기는데요. 내가 왜 이 디자인패턴, 아키텍쳐를 선택했는지 다시 한 번 리마인드 해보는 것도 중요한 것 같다는 생각이 들었답니다.
클린아키텍쳐를 설명하는 글들을 보면 굉장히 많은 레이어로 구성된 것을 볼 수 있는데요.
추후 서비스의 규모가 커지는 경우 등을 대비하여 이 레이어를 모두 구현해봐도 좋겠지만 내게 주어진 시간이 없다면 이 중 정말 필요한 레이어만 선택하는 것이 좋을 것 같아요.
또한 내가 생각하는 레이어의 정의를 명확히 한다면 리팩토링 하면서 도대체 이 역할을 하는 객체는 어떤 레이어에 포함되어야하는거지? 하는 생각으로 고민하는 시간을 확 줄일 수 있으실거에요 (저는 이걸 지금에야 깨달았습니다 ㅜ)
저는 이렇게 리팩토링을 시작했어요.
- DTO 역할을 하고 있는 타입 찾기
- Entity 설계하기
- DTO를 Entity로 Mapping 하기
- UseCases 정의하기
- Repository 정의하기
Entity가 가장 고수준에 있는 레이어이기 때문에 이걸 먼저해야하나 싶은 생각이 들었는데, 저는 네트워킹후 디코딩하는 타입을 그대로 ViewModel이 가지는 data로도 사용하고 있었기 때문에 이에 대한 분리가 먼저 필요하다고 생각했어요.
저의 경우엔 아래와 같이 레이어를 구성하고 정의내렸어요.
- Domain 레이어
- UseCases
- RefreshClimbingRecordUseCase
- Data 레이어
- Entity
- MonthRecordEntity
- ClimbingRecordDate
- Repository
- RecordRepository
- Presentaion 레이어
- 해당 레이어는 구현해야하는 필요성을 아직 찾지 못해서 분리하지 않음.
- Infrastructure
- NetworkService
다른 예제코드 및 프로젝트를 보면 Storage, Presenter 등 다양한 역할의 타입을 구현한 것을 볼 수 있었는데요. 저의 경우 주어진 시간이 짧기도 했고, 앱의 규모가 큰 편이 아니었어요.
여러분들도 다양한 프로젝트를 살펴보면서 여러분만의 클린아키텍쳐 구조와 레이어를 설계해보는 재미를(?!) 느껴보시길 바랄께요!!
사실 이 클린아키텍쳐를 공부하게 된 가장 큰 이유는 앱의 출시만을 목적으로 진행한 사이드프로젝트가 항상 마음에 찝찝하게 남았기 때문이에요.

변명 같지만(또 사실 생각해보면 변명이기도 하죠) 본업을 하면서 사이드를 하다보니 앱 내부 구조와 디자인 패턴에 대해 집중하는 시간을 가지기 정말 어렵더라구요. 특히 프로젝트 기간이 늘어나면서 저와 팀원들 모두 지치다보니 내부구조는 나중에 생각하고 우선 출시하자!!! 를 목표로 하기도 했구요.
정말 일부 기능만 클린아키텍쳐의 특징을 적용하여 리팩토링 했지만 앞으로 남은 기능도 리팩토링하는것이 저의 목표에요!
위 글에서 개념오류를 발견하시거나 같이 논의해보고 싶은 주제가 있다면 언제든지 환영입니다 :)
긴 글 읽어주셔서 감사합니다.

https://velog.io/@ddophi98/클린-아키텍쳐
https://jerry311.tistory.com/73
https://jaime-note.tistory.com/406
https://medium.com/@apfhdznzl/data-layer-repository-datasource-9621d73b6144
https://blog.coderifleman.com/2017/12/18/the-clean-architecture/