2024 Apple Developer Academy 3기

[한배:국악 매트로놈] 클린아키텍처 첫 도입기

It’s me. Right. 2024. 11. 1. 20:07
반응형

처음 Swift 개발을 시작했을 때는 문법을 배우고 앱을 개발하는 프로세스에 집중을 하게 된다. 

지금까지 나는 이러한 상황 속에 있었고, 주변에 있는 경험많은 개발자들이 아키텍처의 필요성에 대한 이야기를 할 때 아직 내가 도달하기엔 어려운 이야기라고 생각했다. 

그리고 드디어 이제는 아키텍처를 도입해도 될 때라는 생각이 들어 마지막 프로젝트에서 클린아키텍처를 적용해보는 과정을 기록해보려고 한다.


1. 시스템 아키텍처란?

시스템 아키텍처는 시스템의 구조를 정의하고 구성 요소 간의 관계를 설계하는 방법을 말합니다. 

 

아키텍처라는 용어를 여기저기서 참 많이 들어보는 것 같은데 대략적인 느낌만 있고, 그 실체를 설명하라고 하면 항상 어렵다고 느끼는 경우가 있을 것이다. 위에서 설명하는 정의가 어렵게 느껴지지만 아키텍처는 무언가를 만들기 위한 설계도와 같은 것을 생각하면 된다. 건물을 짓는다면 건물의 건축도면이 예시가 되는 것처럼 시스템을 만들기 위한 시스템도면이 아키텍처이다.

 

시스템 아키텍처에는 여러 아키텍처 스타일(예: 마이크로서비스, 계층형, 클라이언트-서버 등)이 있다. 우리가 어떤 것들을 구현해야 하는지, 어떤 것에 중점을 둘지에 따라서 여러 아키텍처중에서 조합을 통해 적용해 나가는 것이다.

[시스템 아키텍처와 건물의 설계]
- 이해를 돕기 위해서 건물의 설계와 아키텍처를 비교해보았어요.

1. 시스템 아키텍처는 건물의 기본 설계도와 같다
- 건축에서는 건물이 어떤 구조로 지어질지를 결정하기 위해 먼저 설계도를 만들고, 설계도는 층의 구조, 방의 위치, 벽과 창문의 위치를 나타내주게 된다. 시스템 아키텍처도 이와 마찬가지로 소프트웨어 시스템이 어떤 구조로 구성될지를 정한다.

2. 레이어드 아키텍처 = 건물의 층을 계획하는 것
- 층의 구조를 나타내는걸 예시로 든다면 건물의 1층은 로비 역할, 2~5층은 상가 역할, 6~11층은 거주 역할로 나누는 것처럼 시스템에서는 데이터베이스 계층, 비즈니스 로직 계층, 사용자 인터페이스 계층 기능별로 층을 나눠서 정의할 수 있다.

3. 마이크로서비스 아키텍처 = 건물 단지를 분리하는 것
- 대학 캠퍼스가 여러 건물로 나뉘어 도서관, 강의동, 연구실, 기숙사 등이 따로 존재하고 각 건물의 역할이 명확하게 정해져 있는 것처럼, 마이크로서비스에서는 사용자 관리, 결제 처리, 주문 관리 등 각 서비스가 독립적으로 운영된다.

4. 이벤트 드리븐 아키텍처 = 건물 내 비상 경보 시스템
- 화재 경보기가 작동하면 비상 벨 울림, 엘리베이터 정지, 비상구로 안내 표시등 켜짐과 같은 행위가 실행된다. 이는 특정 이벤트가 발생할 때마다 이에 반응하는 시스템이 작동하는 구조로 시스템 아키텍처에서도 사용자 동작이나 외부 이벤트에 따라 관련 서비스들이 작동되도록 구현하는 것을 의미한다. 

 


2. 클린 아키텍처란 무엇인가?

클린 아키텍처는 시스템 내부의 코드 구조와 모듈화를 체계적으로 설계하는 데 중점을 둔 아키텍처 패턴입니다.

 

주요 목표는 의존성 규칙에 따라 비즈니스 로직과 외부 요소(예: 데이터베이스, UI)를 분리하여, 코드의 변경과 확장이 용이하도록 만드는 것입니다. 이는 시스템 아키텍처의 설계 패턴 중 하나로 사용될 수 있습니다.
클린 아키텍처는 시스템 아키텍처 내에서 사용될 수 있는 설계 원칙으로, 모듈 간의 의존성을 관리하고 비즈니스 로직을 외부 요소로부터 분리하는 데 초점을 둡니다.

모듈이란? 자신의 역할에 맞게 분리된 하나의 기능만을 담당하는 코드파일 혹은 그룹이라고 생각한다.
[클린 아키텍처와 건물의 설계]
- 클린 아키텍처는 건축물의 내부 기능 배치와 구조 설계에 가까운 개념이라고 이해하면 된다. 각 기능이 독립적이면서도 필요한 데이터나 흐름을 원활하게 공유할 수 있게 하며, 변화나 확장에 유연하게 대응할 수 있는 구조를 만들도록 돕는 역할을 한다.

1. 기능 분리 및 모듈화: 내부 공간의 역할 구분
- 클린 아키텍처는 시스템의 각 기능을 독립적으로 분리하고, 핵심 비즈니스 로직과 구체적인 구현(예: 데이터베이스, UI) 간의 의존성을 분리한다. 건물 내부의 각 방이나 구역이 서로 독립적인 역할을 수행하도록 설계하는 것과 비슷하다고 할 수 있다.
- 병원 건물에서는 진료실, 수술실, 응급실이 각기 다른 기능을 수행하지만 서로 연계되어 각 역할을 올바르게 수행했을 때 환자가 필요로하는 진료, 수술 등을 거쳐 치료의 길로 나갈 수 있다. 진료실과 수술실은 맡은 역할이 명백하게 다른 기능을 수행하지만, 필요할 때 서로의 데이터를 공유할 수 있는 구조가 있어야 원활한 프로세스가 되는 것처럼 클린 아키텍처에서도 각 모듈이 독립적으로 기능하되 필요한 정보는 교환할 수 있게 설계해야 함은 동일한 것이다.

2. 의존성 역전: 주요 기능이 구체적인 부분에 의존하지 않도록 설계
- 클린 아키텍처의 핵심 원칙인 의존성 역전 원칙은, 중요한 비즈니스 로직이 구체적인 외부 구현에 의존하지 않도록 설계하는 것을 목표로 하고 있다.
- 위에서 병원 건물을 예시로 들었으니 계속 해보자면 건물에서 진료실의 전기 배선의 일부분을 변경해야 한다고 했을 때, 수술실 및 중환자실을 포함한 병원 전체의 전기를 내리고 진행을 해야한다고 하면 뭐라고 할 것인가? 당연히 그렇게 진행되는 것은 허용할 수 없습니다!라고 할 것이다.
- 이처럼 중요한 공간의 경우 전기 배선 등의 요소들이 변경되더라도 전체적인 기능에 영향을 받지 않도록 구조를 만들고 배치하는 것처럼 클린 아키텍처에서도 핵심적인 로직이 주변의 변경될 가능성이 많은 요소에 종속되지 않도록 하는 것이 중요하다.

4. 유연성과 확장성: 공간 변경이 쉽게 이루어질 수 있도록 설계
- 클린 아키텍처는 코드의 유연성과 확장성을 중요하게 여기며, 이 때문에 특정 부분을 수정하거나 확장할 때 다른 부분에 영향을 최소화하는 설계를 하는 것을 목적으로 한다. 이는 건축물에서 공간의 용도나 구조를 쉽게 변경할 수 있도록 설계하는 것과 유사하다.
- 설계를 아무리 완벽하게 했다고 생각해보 구현을 하다보면 수정사항이 단 한건도 없기는 쉽지 않다. 따라서 이런 변경에 대한 니즈가 존재한다는 가정하에서 변경 시 들어가는 리소스를 최소화 해야 함은 틀림 없이 중요한 사항이다.
- 병원의 진료실을 확장할 필요가 생기면 다른 공간의 영향을 최소화하면서 쉽게 확장할 수 있도록 초기 설계에서 여유 공간을 두거나, 벽을 이동 가능한 파티션으로 설계하는 것과 같다.

 

2-1. 클린 아키텍처의 주요 원칙

 

1) 경계(Boundary)의 분리  
- 시스템을 여러 영역으로 나누고, 각 영역 사이의 인터페이스를 정의하여 각 영역의 독립성을 보장한다.

- 가장 핵심적인 비지니스 로직이 안쪽 계층에 존재하고, 경계를 명확히 분리하여 안쪽 계층이 외부 요소의 변경에 영향을 받지 않고 시스템의 핵심을 유지할 수 있도록 해야 한다.

 

2) 의존성 역전 원칙(Dependency Inversion Principle)  
- 고수준 모듈은 저수준 모듈에 의존해서는 안되며, 양쪽 모듈 모두 추상화에 의존해야 한다.

"의존한다"의 정의: A가 B에 의존한다라는 말은 B를 변경하면 A도 B의 변경사항에 맞춰서 변경되어야 함을 의미한다.


- 클린 아키텍처에서는 의존성은 밖에서 안으로 흘러야 한다고 이야기 하고 있다. 즉 원의 내부 계층에서 외부 계층을 의존하면 안된다는 의미이다.
- 핵심 비즈니스 로직과 유즈케이스는 외부 요소(UI, 데이터베이스, 네트워크 등)에 의존하지 않고, 오히려 외부 요소가 내부 계층의 인터페이스를 구현하도록 하여 의존성을 반대로 설정하도록 해야한다.


3) 인터페이스 분리 원칙(Interface Segregation Principle)  
-  클라이언트가 자신이 사용하지 않는 메서드에 의존하지 않아야 한다. 즉, 인터페이스는 클라이언트의 요구에 딱 맞는 형태로 분리되어야 한다.

 

2-2. 내가 생각하는 클린 아키텍처

 

클린 아키텍처의 지향점
1) 시스템의 유지보수성 2) 변경 유연성 3) 테스트 용이성

 

- 내가 생각하는 클린 아키텍처가 지향하는 점은 크게 3가지라고 생각한다. 그 중에서도 시스템의 유지보수성을 고려한 부분이 돋보인다고 생각한다. 

- 이유 1: 유지보수를 원활하게 하기 위해서는 서로 얽혀있는 것보다 각각이 분리되어 있는 것이 중요하다. 이를 위해서 Boundary 분리를 통해서 계층, 각 계층 내에서도 기능별로 분리하도록 설계가 필요했다고 생각이 들었다.

- 이유 2: 이렇게 분리를 해놨지만 서로 필요한 상황이 존재하고 유기적으로 활용되어야 한다. 이렇게 상호작용이 필요한 상황에서 의존에 의한 연쇄적 변경을 최소화화기 위해 인터페이스 즉 추상화라는 매개체를 의존함을 통해 자체적 의존성을 줄이는 방식을 고안한 것이라고 생각한다.

- 이유 3: 시스템의 부분을 변경한 후에는 시스템이 기능별 명세에 맞게 올바르게 작동하는지 확인하는 테스트 작업이 필요하다. 시스템이 분리되어있는 경우 이런 테스트 작업에 있어서 시스템 전체가 아닌 부분적으로 테스트가 가능하고, 이 것은 궁극적으로 최종 테스트를 포함한 유지보수적인 측면에서 리소스를 줄일 수 있는 방법이라고 생각한다.

 

따라서 지속적이고 장기적인 유지보수적 측면에서 효율성을 고려한 아키텍쳐라는 생각을 하게 되었다.


3. 클린 아키텍처 구현기

1) 서비스 소개

- 국악으로 음악에 입문해서 서양음악 체계에 익숙하지 않은 국악 연주가가 혼자서 기본기 연습을 할 때 사용하는 국악 음악체계(장단별 특징, 용어)를 반영한 메트로놈 앱

 

2) MVP Feature List

- 우리팀에서 MVP로 구현하고자 하는 기능을 아래와 같이 정의하였다.

 

3-1. 한배 서비스에 클린 아키텍처를 적용하게 된 이유

- 우리 팀은 국악 서비스를 제공하지만 팀원 내 국악기를 연주하는 사람이 1명뿐이고, 국악기 자체보다 사물놀이의 쇠(꽹가리) 연주자에 가깝다보니 우리의 솔루션이 정말로 타겟한테 필요한 솔루션인지를 검증하는 작업을 지속적으로 필요로했다.

- 따라서 UT를 통해서 UX 사용성, 기능의 필요성을 검토하고 수정하는 작업 또한 지속적으로 이뤄질 것으로 예상하였기에 유지보수적인 측면에서 효율적인 구조를 가져가야 한다고 생각했다.


1) UseCase 정의

- Feature List를 통해서 사용자가 우리 서비스를 통해서 어떤 행위를 수행할지에 대해 정의해보았다.

 

 

위에 정의된 Domain Layer의 내용을 바탕으로 우리 앱의 구조가 결정되었다. 각 과정에서 클린 아키텍처가 지향하는 바를 구현하였다.

3-2. 한배 서비스의 클린 아키텍처 적용의 첫번째 스프린트를 마치고,

1) 결론적으로 구현된 클린 아키텍처

  • 인터페이스 사용을 통한 UseCase간의 의존성 분리
  • 시스템 경계의 분리

2) 아직 적용되지 않은 것( = 앞으로 고민해봐야 할 것)

 

1. DI Container

  • 현재 우리앱은 시작 시 MainView에서 장단 리스트를 보여주는 형식으로 구현되어 있다. 이 경우 ForEach에서 각 jangdan에 해당하는 view를 호출해주고 있고, View의 initializer에서 viewModel을 생성하고 있기 때문에 ViewModel이 장단의 개수대로 생성되고 있다.
  • 현재 viewModel 내부적으로도 다수의 UseCase를 의존성 주입 없이 사용하고 있기 때문에 jangdan * viewModel 내부 UseCase 객체의 init을 생각하면 의존성 주입을 하는 것이 좋을 것 같다는 생각이 들었다.

2. Data Layer 구조 변경

  • 현재 Repository는 template에 대한 데이터를 보관하고 있는 상황에서 Repository의 의미를 조금 다르게 사용하고 있다고 생각한다.

3) 1차 스프린트를 통해 느낀점

  • 처음에 우리 서비스에서 필요하다고 생각한 기능을 정의하고 기능과 구현 프로세스를 생각하면서 아키텍처를 설계했다고 생각했다. 구현을 했으니 클린 아키텍처만의 장점을 누려봐야 하지 않겠는가? 각 기능별, 즉 UseCase별로 개발 전에 주어진 기능명세에 따른 테스트를 진행해보고자 하였다!
  • 테스트를 진행해보니 생각보다 기능별로 분리가 완전히 되어있는게 맞나? 라는 생각이 들었던 부분이 있어서 이런 부분들도 다시 한번 클린 아키텍처 구조에 따라서 수정을 해야할 것 같다는 생각이 들었다.

3-3. 두번째 스프린트의 시작, 그리고 첫번째 변화 : 데이터 관리 주체의 변경

1) 새로운 기획 추가

  • 1차 스프린트를 마치고, 예술고등학교와 국악 명인분들을 찾아가 직접 사용을 부탁드리고 피드백을 받았다. 그 결과 추가적인 기획이 생기면서 기능을 추가하게 되었다.

 

2) 장단데이터UseCase에서 사용중인 데이터의 관리 책임분리

https://github.com/DeveloperAcademy-POSTECH/2024-MacC-A18-Krabs/pull/119

https://github.com/DeveloperAcademy-POSTECH/2024-MacC-A18-Krabs/pull/124

https://github.com/DeveloperAcademy-POSTECH/2024-MacC-A18-Krabs/pull/148

  • 앞선 내용처럼 우리가 처음 아키텍처를 정의했던 구조에 의한다면 "장단데이터UC"가 하는일은 다음과 같다
    1. 장단Repository로 부터 불러온 데이터를 객체로 저장해서 앱의 생명주기 내에서 사용되는 데이터의 위치가 됨
    2. ViewModel에서 강세변경UC, 속도변경UC 변경 요청이 들어오면 실제 데이터를 변경하고 Combine으로 데이터를 배포하는 역할을 함
  • 기존의 장단데이터Repository는 단순히 장단데이터UC에서 사용할 데이터 원본을 가지고 있는 역할을 하고 있다. 그렇다면 Repository하기보다 DB의 역할에 가깝다. 따라서,
    1. 기존 Repository 역할 -> struct BasicJangdanData로 파일 분리
    2. Repository는 다양한 DB로부터 들어오는 데이터를 일정하게 처리할 수 있는 구조로 설정, CRUD에 알맞은 데이터 구조로 기능을 수행할 수 있도록 정의함
    3. SwiftData를 사용하기 때문에 장단데이터Manager를 구현체로 생성해 하드코딩된 기본장단데이터와 로컬 스토리지에 저장되는 커스텀장단데이터를 받아와서 내부적으로 사용하도록 기능을 구현함

변경 전 아키텍처
변경 후 아키텍처

 

3) 장단 강세 데이터 구조 변경

 

  • 기존에는 모드 데이터를 TemplateUseCase가 관장했기 때문에 소박(대박) 모드의 변경사항에 따라서 필요한 데이터만 ViewModel로 전달해주는 방식을 사용하였다.

  • 하지만 우리 앱은 장단의 특성을 반영해서 사용자가 이해하기 쉽도록 일정한 규칙을 가지게 하려면 '한 배'라는 국악의 특징을 반영할 수밖에 없었다. 따라서 장단별로 뷰를 그리는데 필요한 정보가 다르고 사용자의 설정에 따라서 변화해야 하는 경우가 많다.
  • 따라서 장단에 대한 정보는 View와 ViewModel에서 일정하게 가지고 있고, 사용자의 설정에 따라서 보여주는 방식 내부적으로 다르게 사용하도록 변경하였다. 이렇게 하면 전체적으로 관리되는 강세 데이터의 형태를 일정하게 유지할 수 있다.
  • 한가지 더 고려해야 하는 부분이 장단 소리 재생에 대한 부분이다. 기존에 TemplateUseCase에서 데이터를 관리할 당시에는 내부적으로 소박(대박) 모드에 따라서 데이터를 변경해서 MetronomeOnOffUseCase에 전달하도록 구현되어 있었지만 현재는 Manager에서 모든 구역으로 동일한 구조의 장단 데이터를 보내주고, 소박(대박) 모드에 따라서 MetronomeOnOffUseCase에서 내부적으로 연산프로퍼티를 통해 사용되는 데이터를 변경해서 사용하도록 구현하였다.

4) DI Container를 사용한 의존성 주입

  • 의존성을 주입하는 이유는 다양하지만 우리에게 DI Container의 사용이 필요했던 가장 명확한 이유는 2가지였다.
  • 첫번째, 책임의 분리
    - 기존에 JangdanList의 Jangdan 종류에 따라서 각각 View를 생성하면서  View에 ViewModel을 주입해주고 있었으며, 이 때마다 생성되는 ViewModel에서 사용하고 있는 여러개의 UseCase들을 매번 새로 생성해서 ViewModel에서 사용하고 있었기 때문에 따로 생성할 필요가 없는 객체들을 한번에 관리하기 위함이였다.
// MARK: DI Continer 적용 전
class MetronomeViewModel {
init() {
        let initJangdanRepository = JangdanDataSource()
        self.jangdanRepository = initJangdanRepository
        
        let initTemplateUseCase = TemplateUseCase(jangdanRepository: initJangdanRepository)
        self.templateUseCase = initTemplateUseCase
        
        let initSoundManager: SoundManager? = .init()
        
        self.metronomeOnOffUseCase = MetronomeOnOffUseCase(jangdanRepository: initJangdanRepository, soundManager: initSoundManager!)
        
        self.accentUseCase = AccentUseCase(jangdanRepository: initJangdanRepository)
        
        let initTempoUseCase = TempoUseCase(jangdanRepository: initJangdanRepository)
        self.tempoUseCase = initTempoUseCase
        
        self.taptapUseCase = TapTapUseCase(tempoUseCase: initTempoUseCase)
        self.taptapUseCase.isTappingPublisher.sink { [weak self] isTapping in
            guard let self else { return }
            self._state.isTapping = isTapping
        }
        .store(in: &self.cancelBag)
        
        self.jangdanRepository.jangdanPublisher.sink { [weak self] jangdan in
            guard let self else { return }
            self._state.jangdanAccent = jangdan.daebakList.map { $0.bakAccentList }
            self._state.bpm = jangdan.bpm
            self._state.bakCount = jangdan.bakCount
            self._state.daebakCount = jangdan.daebakList.count
        }
        .store(in: &self.cancelBag)
    }
}

ForEach(jangdanList) { jangdan in
     MetronomeView(viewModel: MetronomeViewModel(jangdan: jangdan))
}
  • 두번째, 데이터의 관리 책임 통일 및 메모리 관리
    -  SoundManager의 경우 장단에 따라서 메트로놈 소리를 출력해주는 역할을 담당하는데, 기존 코드에서는 각 장단에서 따로 ViewModel을 생성하고 ViewModel에서 SoundManager를 또 따로 생성해서 사용하고 있었다. 따라서 하나의 장단에서 Out하는 시점에서 기존의 SoundManager를 중지하거나 해지하지 않고 빠져나오는 경우 소리가 다른 뷰로 옮겨가도 소리가 중단되다는 보장을 할 수 없었고, 여러 진입점이 있었던 기획 시점에서 실제로 다른 장단으로 옮겨갔는데 이전 장단의 소리가 꺼지지 않는 등의 오류가 발생하였다.
    - 이런 상황에서 SoundManager는 한번에 하나의 장단의 소리만 출력하는 역할을 하기 때문에 데이터를 관리하기 수월하다는 장점을 얻을 수 있었다. 
// MARK: DI Continer 적용 후
class DIContainer {
    
    static let shared: DIContainer = DIContainer()
    
    private init() {
        self._appState = .init()
        self._jangdanDataSource = .init(appState: self._appState)!
        self._soundManager = .init(appState: self._appState)!
        
        self._templateUseCase = TemplateImplement(jangdanRepository: self._jangdanDataSource, appState: self._appState)
        self._tempoUseCase = TempoImplement(jangdanRepository: self._jangdanDataSource)
        self._metronomeOnOffUseCase = MetronomeOnOffImplement(jangdanRepository: self._jangdanDataSource, soundManager: _soundManager)
        self._accentUseCase = AccentImplement(jangdanRepository: self._jangdanDataSource)
        self._tapTapUseCase = TapTapImplement(tempoUseCase: self._tempoUseCase)
        
        self._metronomeViewModel = MetronomeViewModel(templateUseCase: self._templateUseCase, metronomeOnOffUseCase: self._metronomeOnOffUseCase, tempoUseCase: self._tempoUseCase, accentUseCase: self._accentUseCase, taptapUseCase: self._tapTapUseCase)
        
        self._controlViewModel =
        MetronomeControlViewModel(jangdanRepository: self._jangdanDataSource, taptapUseCase: self._tapTapUseCase, tempoUseCase: self._tempoUseCase)
        
        self._homeViewModel = HomeViewModel(metronomeOnOffUseCase: self._metronomeOnOffUseCase)
    }
    
    // ViewModel
    private var _metronomeViewModel: MetronomeViewModel
    var metronomeViewModel: MetronomeViewModel {
        self._metronomeViewModel
    }
    
    // controllerViewModel
    private var _controlViewModel: MetronomeControlViewModel
    var controlViewModel: MetronomeControlViewModel {
        self._controlViewModel
    }
    
    private var _homeViewModel: HomeViewModel
    var homeViewModel: HomeViewModel {
        self._homeViewModel
    }
  

    // UseCase Implements
    private var _templateUseCase: TemplateImplement
    private var _tempoUseCase: TempoImplement
    private var _metronomeOnOffUseCase: MetronomeOnOffImplement
    private var _accentUseCase: AccentImplement
    private var _tapTapUseCase: TapTapImplement
    
    private var _jangdanDataSource: JangdanDataManager
    private var _soundManager: SoundManager
}

class MetronomeViewModel {
    private var jangdanRepository: JangdanRepository
    
    private var templateUseCase: TemplateUseCase
    private var metronomeOnOffUseCase: MetronomeOnOffUseCase
    private var tempoUseCase: TempoUseCase
    private var accentUseCase: AccentUseCase
    private var taptapUseCase: TapTapUseCase
    
    private var cancelBag: Set<AnyCancellable> = []
    
    init(jangdanRepository: JangdanRepository, templateUseCase: TemplateUseCase, metronomeOnOffUseCase: MetronomeOnOffUseCase, tempoUseCase: TempoUseCase, accentUseCase: AccentUseCase, taptapUseCase: TapTapUseCase) {
        
        self.jangdanRepository = jangdanRepository
        self.templateUseCase = templateUseCase
        self.metronomeOnOffUseCase = metronomeOnOffUseCase
        self.tempoUseCase = tempoUseCase
        self.accentUseCase = accentUseCase
        self.taptapUseCase = taptapUseCase
        
        self.taptapUseCase.isTappingPublisher.sink { [weak self] isTapping in
            guard let self else { return }
            self._state.isTapping = isTapping
        }
        .store(in: &self.cancelBag)
        
        self.jangdanRepository.jangdanPublisher.sink { [weak self] jangdan in
            guard let self else { return }
            self._state.jangdanAccent = jangdan.daebakList.map { $0.bakAccentList }
            self._state.bpm = jangdan.bpm
            self._state.bakCount = jangdan.bakCount
            self._state.daebakCount = jangdan.daebakList.count
        }
        .store(in: &self.cancelBag)
    }
}

ForEach(Jangdan.allCases, id: \.self) { jangdan in
         NavigationLink(destination: MetronomeView(viewModel: DIContainer.shared.metronomeViewModel, jangdan: jangdan)) {
             Text("\(jangdan)")
    }
}

3-4. 세번째 스프린트의 시작

1) PresentLayer ViewModel에서 DataLayer Repository 분리

  • 사실 Repository를 분리하는 것에 대해서 많은 논의가 있었고, 결국 분리하는 것으로 결정을 하게 되었다. 
    • 1. 의존성 측면
      - Repository 프로토콜를 따라서 DI Container로 주입받고 있었기 때문에 직접적인 객체에 의존하는 문제는 이번 결정에 영향을 주지는 않았다고 생각한다.
      - 하지만 Domain Layer의 UseCase, Presenter Layer의 ViewModel이 모두 Repository를 들고 있었고, 2개의 Layer에 영향일 미치는 것은 유지보수적인 측면에서 여러 부분을 변경해야 할 수도 있다고 생각하기 때문에 데이터를 관리하는 방식을 변경해야 겠다고 생각했다.
  • 기존에 Repository에서 직접 MetronomeViewModel로 받아오는 데이터는 1)장단별 강세 데이터, 2)장단의 기본 타입값 이렇게 2개였다.
    • 1) 장단별 강세 데이터
      - 강세변경UC(AccentUseCase)를 통해서 Combine으로 받아오고, ViewModel에도 Combine으로 Publish해주도록 변경하였다.
    • 2) 장단 기본 타입값
      - 장단명, 장단 타입, 장단 템플릿 등 기본적으로 장단단위의 데이터를 관리하는 장단데이터UC(TemplateUseCase)를 통해서 받아오는 것으로 처리하는 로직으로 변경하였다.
    • 추가적으로 MetronomeControlViewModel에서 따로 관리중인 BPM 데이터도 속도변경UC(TempoUseCase)를 통해서 데이터를 받아올 수 있도록 변경할 예정이다.

  • 이 과정에서 가장 많이 나눴던 이야기가 유지보수적 효율성 VS 코드의 효율성에 대한 이야기였다. UseCase에서 따로 데이터를 다른 형식으로 파싱하는 과정을 거치지 않는다.
    • 지금은 하나의 데이터를 여러 UseCase에서 접근할 일이 없지만, 예를들어 BPM 데이터를 3개의 UseCase에서 사용하고 데이터 값을 변경한다면 각 UseCase <-> Repository, UseCase <-> ViewModel 사이의 데이터 관계를 전부 관리해 줘야 한다. 그래서 Repository에서 변경된 데이터를 UseCase를 거치지 않고 ViewModel로 보내주는게 더 효율적이지 않을까?하는 생각이 들었다. 하지만 클린 아키텍처에서 테스트와 유지보수적 관점에서 그리고 데이터의 일관적 관리 관점에서 생각했을 때는 이렇게 구조를 가져가는게 적합하다는 생각이 들었다. 그리고 논의하면서 클린아키텍처가 유지보수적 관점의 아키텍처이기 때문이라는 말을 들었는데 반박할 수가 없었다... 잊지 말자 나는 클린 아키텍처를 기준으로 고민해야 한다는 사실을...
    • 이 과정에서 UseCase에서 ViewModel로 넘겨주는 데이터의 형식에 대해서 조금 고민이 되는데, View에서 사용될 데이터만 파싱해서 넘겨주는 것이 맞는지, View에서 사용될 데이터는 ViewModel이 관장을 하고 우리는 정해진 Entity 형식을 그대로 넘겨주는 것이 더 적절한지에 대한 고민이다. View에서 사용될 데이터는 ViewModel이 View의 상황에 따라서 변경하는 것이 더 적합하다고 생각한다. 하지만 혹시 좋은 의견이 있다면 의견 남겨주세요...

2) Router, AppState DIContainer 분리

  • 기존에 AppStorage를 통해서 관리하던 앱 설정 값을 관리하는 따로 모아서 관리하는 AppState를 만들어주었다. 우리 앱에서 관리하는 설정값은 1)앱 첫 진입 여부, 2) 앱에서 제공하는 메트로놈 실행 시 악기소리(북 or 장구) 3) 메트로놈 실행 시 비프음 여부 이다.
    • 위 2,3의 상태 값의 경우 Presenter, Domain, Data Layer 모든 Layer에 걸쳐서 사용되는 데이터이다. 기존에는 사용이 필요한 곳에서 @AppStorage 프로퍼티 랩퍼를 통해서 사용하고 있었다. 
    • 하지만 다양한 곳에서 사용되는 만큼 해당 데이터 값이 바뀌게 된다면 모든 곳을 찾아다녀야 했기 때문에, 앱 전체에서 설정 값을 관리하는 AppState를 만들고 DI Container를 통해서 사용하도록 변경하여 코드의 일관성과 관리의 효율성을 높이고자 하였다.

4. 클린 아키텍처 필요성을 느꼈는가?

- 굳이....? 소규모 프로젝트에서 필요한가? 앱의 규모가 작았기 때문의 이슈라고 생각한다. 

- 아직은 정말 대규모로 기능별로 나눌만한 개발을 해보지 않아서 View, ViewModel, Model, Repository만 분리하고 의존성을 크게 고려하지 않았던 경우에도 변경사항에 어려움이 없었던 것 같다.

- 앱 하나만을 기반으로 유지보수 작업을 진행하면서 유용성, 필요성에 대해서 크게 와닿지 않음이 있다. 하지만 클린 아키텍처가 적용되지 않은 프로젝트와 적용된 프로젝트를 같은 시기에 유지보수하고 배포를 해보니 확실히 클린 아키텍처가 적용된 프로젝트가 변경사항을 반영하는데 있어서 비교적 수월함을 느꼈던 것 같다.

  • 정말 처음에는 클린 아키텍처의 필요성이 있나? 별 차이가 없는데?라고 진심으로 느꼈었다. 하지만 장단데이터, 메트로놈 소리를 재생한다와 같은 기본 기능을 제외한 부분에서 추가되고 삭제되는 기획이 많아질수록 기능을 변경하기 위해서 내가 확인해야하는 코드와 파일의 단위가 명확해지고 이를 통해서 업무 계획과 범위를 파악하는데 매우 수월하다고 느꼈던 것 같다.
  • 앱 쇼케이스 직전 개발된 앱에 대해서 내부적으로 디자인팀과 디자인 및 기능에 대한 QA를 진행하였다. 수정이 필요한 내용이 약 35개정도 되었는데 물론 디자인 적인 부분도 있었지만 데이터 구조를 바꾼다던가 데이터 로직의 오류가 발생하는 부분을 포함해서 수정하는데 3시간도 걸리지 않았다. 문제가 발생하는 부분이 있으면 어느 부분에서 문제가 발생할 수밖에 없을지를 쉽게 파악할 수 있었기 때문에 빠르게 확인하고 반영할 수 있었다고 생각한다. 나는 이런 프로세스가 가능한 이유가 아키텍처가 잡혀 있었기 때문이라고 믿어 의심치 않는다.

4-1. 취준, 초년생 개발자로서 클린 아키텍처를 알아야하는 이유

- 웹개발자로 첫 인턴을 하면서 칭찬을 받았던 적이 있다. 내가 개발하고자 하는 서비스를 운영하는 서버비용에 대한 계산을 같이 보고했고, 회사가 필요로 하는 정보가 무엇인지 고려해본 자세에 대한 칭찬이였다. 클린아키텍처도 그런 관점에서 개발자를 하려고 하는 사람이 가지고 있으면 좋은 개념이라고 생각한다.
- 회사는 소규모의 단기적인 프로젝트보다 지속적으로 운영할 수 있는 서비스를 필요하다고 생각한다. 기존에 운영되는 서비스와 유지보수를 통해 신규로 필요한 기능을 추가 및 삭제를 진행하면서 안정적으로 운영을 하기는 원할 것이다. 단기적인 관점에서는 서비스를 개발만 하고 사용자가 사용할 수 있도록 배포하는 것이 중요하지만, 장기적인 관점을 생각한다면 언제든지 누구나 쉽게 수정할 수 있도록 기능을 분리해놓은 설계도 기반 구현이 중요할 수 밖에 없는 것 같다.
- 따라서 회사의 입장에서는 이런 설계를 이해하고 맞춰서 유지보수를 해줄 능력이 있는 개발자를 고려하게 될 것이기 때문에 알아두면 분명히 도움이 될 것이라고 생각한다.

 

5. 이후 계획

- 아키텍쳐적으로 고민되는 부분들이 꽤 많이 있다. 의존성 분리가 더 필요한 곳도 있고, 유닛 테스트를 진행하고 싶기 때문에 테스트 용의성을 고려해본다면 분명히 수정해야 하는 부분들이 꽤 있을 것 같다. 

- 그리고 추가적으로 기능들이 들어오거나 하는 과정에서 클린 아키텍쳐를 설계한 이점을 더 많이 누려보고 싶다. 

 

[논의 맟 개선해보고 싶은 내용들]

  1. AppState와 UserDefaults의 의존성 분리 여부
  2. DI Container 싱글톤 구조와 Lazy init 구조의 도입
    - 모든 의존성을 미리 생성하고 있는 구조이기 때문에 메모리 관리적인 측면에서 제어가 필요할 것 같음
  3. ViewModel 생성 등 공통 구조의 중복 제거 방식 -> 팩토리 패턴(?)
  4. Router 사용 여부 및 사용 방식

https://github.com/DeveloperAcademy-POSTECH/2024-MacC-A18-Krabs

 

GitHub - DeveloperAcademy-POSTECH/2024-MacC-A18-Krabs: Pawn Lullu Hazel Tran Eddie Tynee

Pawn Lullu Hazel Tran Eddie Tynee. Contribute to DeveloperAcademy-POSTECH/2024-MacC-A18-Krabs development by creating an account on GitHub.

github.com

코드 리뷰는 언제든지 대환영입니다!!!! 어떤 의견이든 남겨주시면 정말로 감사하겠습니다!!!


기타 참고

- CJENM 안드로이드 프로젝트 도입기

 

왜 Android 신규 프로젝트는 클린 아키텍처를 도입하였는가

소프트웨어 개발은 복잡한 문제를 해결하기 위해 코드를 작성하는 것 이상을 필요로 합니다. 단순히 결과론으로 화면을 보여주는 것만 생각하고 개발한다면, 코드는 점차 커지며 나중에는 손볼

medium.com

 

 

- 클린 아키텍처 계층 구조

 

Swift - Clean Architecture

복잡복잡 클린 아키텍쳐 적용기.

velog.io

반응형