도담도담 Android 아키텍처를 설계하며! 🏗️

최민재·2023년 1월 28일
14

공부

목록 보기
6/6
post-thumbnail

시작하며 ⭐️

일단 저는 대구소프트웨어마이스터고등학교의 초보 Android 개발자입니다! 교내 동아리 B1ND의 Android팀 소속으로 교내 학생 기숙사 생활 관리 서비스 '도담도담' 을 개발하고 있습니다.

도담도담은 B1ND팀에서 꾸준히 유지보수가 되고 있습니다. 저는 B1ND 팀의 6기로 도담도담 Android V6 개발을 했습니다. 그리고 현재 2023년 저는 '도담 Teacher' 라는 선생님 버전의 도담도담 V3를 개발하고 있습니다.

저는 2021, 2022, 2023 3개의 프로젝트를 하면서 각각 아키텍쳐를 설계했습니다. 이제부터 그 과정을 기록하면서 복습해 보겠습니다! 🔥

2021. GLASS 🥛

고등학교 1학년, B1ND팀 인턴 시절.. Web 2명, Android 1명, Server 2명, iOS 1명으로 이루어진 1학년 인턴들과 함께 교내의 소통을 담당하는 인스타그램 서비스 GLASS 를 야심차게 개발했습니다.
먼저 GLASS를 통해 디자인 패턴과 관련된 이야기를 작성하겠습니다!

GLASS를 개발하며.

그때는 Android를 시작하고 약 3..4개월? 정도 되었을 병아리 시절이었죠.. 지금 생각하면 후회되고 부끄러운 이야기지만 GLASS를 시작할 때, 어떤 논리적인 이유도 없이 선배에게 들은 MVVM 디자인 패턴을 적용하려고 했습니다.

음.. 첫 프로젝트인데 좀 멋있는 걸로 하자!!

그때는 MVVM이라는 이름이 정말 멋있어 보였습니다..
그리고 프로젝트를 함에 있어서 공부와 경험의 목적이 많이 컸습니다.

하지만 다른 팀들은 서버랑 서로 값도 주고 받고 멋있게 개발을 하는 동안 경험과 지식이 부족한 저는 계속 선배에게 물어보고 교육도 받으면서 힘들게 코드를 짜고 있었습니다. 정말 느린 속도였습니다.

한번 서버에서 값을 받아와서 뷰를 그리는 코드를 작성하니 구조도 어느 정도 잡히고 어떻게 코드를 짜야 하는지 대충 감이 잡혔습니다.
그 당시에 저를 많이 도와준 선배님들과 유튜브의 MVVM Movie App 강의에게 너무 감사합니다!

당시의 패키지 구조 입니다.

결과

GLASS는 배포 후, 전시회에서도 무사히 전시를 했습니다.
하지만 저는 급하게 개발을 한다고 코드를 짜는 방법만 익혔습니다.
왜 ViewModel을 사용했는지, MVVM 디자인 패턴을 적용하면 어떤 이점이 있는지 전혀 알지 못했습니다.
그렇게 저는 MVVM을 제대로 공부하게 되었습니다.

MVVM은? (Model-View-ViewModel)

MVC(Model-View-Controller), MVP(Model-View-Presenter) 패턴 다음으로 나온 것이 MVVM 디자인 패턴입니다.

MVC는 Activity(or Fragment)에 View, Controller 부분이 함께 구현되기 때문에 상당히 결합도가 높은 패턴입니다.
결합도가 높다는 것은 하나의 변경에 같이 변경되는 것이 많다는 이야기입니다.
결국 유지보수하기 힘들다는 것을 의미하죠!

View, Controller의 Model에 대한 의존성이 강하여 테스트하기 힘들었습니다. 그래서 비즈니스 로직을 분리하기 위해 나온 것이 MVP 패턴입니다.


MVP는 View의 Model에 대한 의존성을 Presenter라는 중개자를 통해서 없앨 수 있는 Model, View, Presenter로 이루어진 디자인 패턴입니다.

여기서 Presenter는
1. View에서 사용자 이벤트 전달 받아,
2. Model에 데이터를 요청하고,
3. 받은 데이터를 View에게 전달합니다.
여기서 Presenter는 View, Model을 모두 참조하고 있습니다.

View도 Presenter를 참조하고 있으니..
View와 Presenter 사이에 강한 의존성이 있다는 문제점이 있습니다!


위의 의존성 문제를 해결하기 위해 나온 것이 MVVM 디자인 패턴입니다.
MVVM은 Model, View, ViewModel로 이루어진 디자인 패턴이죠.

위의 그림을 보시면 ViewModel에서 UI를 조작하지 않습니다.
View가 ViewModel의 데이터를 관찰하여 UI를 조작하죠!

GLASS 프로젝트에서는 LiveData를 사용하였습니다.

LiveData는 관찰 가능한 데이터 홀더 클래스입니다.
LiveData는 수명 주기를 인식하여 메모리 누수가 발생하지 않습니다.


앱 구성요소, 즉 Android 4대 컴포넌트인 Activity, Service, Broadcast Receiver, Content Provider 그리고 Fragment는 모바일에서 사용할 수 있는 리소스의 한계로 운영체제가 언제든 제거할 수 있습니다.
따라서 Activity, Fragment에서 보여지던 데이터가 언제든지 사라질 수 있다는 말입니다.

따라서 앱 구성요소에 데이터나 상태를 저장하는 것은 피하는 것이 좋습니다.

출처: Android 공식 문서
이렇게 ViewModel의 수명주기를 보면 Activity or Fragment의 생명주기가 변함에 상관없이 데이터를 저장할 수 있습니다.

이렇게 MVVM 패턴을 사용하면 유지보수성이 상당히 좋아집니다.

View와 Model 사이에 대한 의존이 ViewModel로 인해 사라졌으며,
ViewModel은 View에 의존하지 않습니다.

의존성이 높다는 것은 한번의 변화로 함께 변경해야 하는 것이 많다는 의미입니다.
이러한 이유로 MVVM 패턴을 사용하면 테스트를 작성하기 용이하며 유지보수성 또한 높아지게 됩니다.

만약 2021년으로 돌아간다면?

저는 GLASS에 MVVM을 적용시킨 이유를 명확하게 했을 것입니다.
GLASS는 학교에서 내부에서 계속 사용하며 유지보수하는 것으로 계획되었습니다.
기능을 세부적으로 자세히 나열하면 거의 10개 이상의 기능이 있었습니다.

"MVVM의 ViewModel에 상태나 데이터를 저장하는 것은 앱 구성요소에 상태나 데이터를 저장하는 것 보다 적합합니다. 그 이유는 ViewModel의 생명주기가 onCreate ~ onDestroy까지 일정하기 때문입니다.
많은 기능이 있는 GLASS는 상태 저장이 용이한 MVVM을 사용하는 것이 적합합니다."

"GLASS는 출시 후 유지보수를 목표로 하는데 MVVM의 의존 관계는 MVC, MVP 패턴의 의존 관계 보다 유지보수성을 높여줍니다. 그 이유는 ViewModel 이 Model과 View 사이의 의존 관계를 끊어주며, View가 ViewModel의 데이터와 상태를 관찰하는 방식으로 ViewModel이 View를 의존하지 않기 때문입니다."

위와 같은 이유로 GLASS에 MVVM을 적용했을 것이었습니다!
당시에 이런 이유를 서술할 수 없었던 저의 얕았던 지식을 후회합니다..

2022. 도담도담 🏫

2022년 저는 겨울 방학 동안 공부를 하며 약간 실력이 늘었습니다.
다양한 해커톤에서 코드를 작성하는 실력도 기를 수 있었죠.
동아리 B1ND의 인턴에서 정직원으로 뽑히기도 했죠! 🎉

저는 도담도담 V6를 개발해야 했습니다.
GLASS와는 다르게 이미 배포 중인 서비스이며 기능도 훨씬 많았습니다.
그리고 팀원 1명이 더 생기며 저는 본격적으로 협업을 할 수 있었습니다.

도담도담은 당연히 계속 이어져 내려오는 서비스인 만큼

  • 유지보수성이 높아야 합니다.
  • 가독성이 좋아야 합니다. (학교 기수를 거듭하는 서비스라서 더욱 필요했죠..)

전 버전의 도담도담은 모바일 Clean Architecture를 적용하고 있었습니다.
하지만 무작정 V6에도 적용할 수는 없었습니다. 먼저 Clean Architecture를 공부했습니다.

Mobile Clean Architecture?


출처 : http://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
위의 그림은 정말 너무 유명해서 개발 공부를 하면서 한번은 보셨을 것입니다.
Robert C. Martin이 제안한 시스템 아키텍처입니다.
기존의 계층형 아키텍처가 가지던 의존성에서 벗어나도록 하는 설계를 제공합니다.

의존성을 의미하는 화살표를 보면 의존성이 밖에서 안을 향하는 것을 알 수 있습니다.
그 말은 바깥 원의 변경이 안의 원에 영향을 끼치면 안된다는 것입니다.


이번 글에서는 이런 Clean Architecture를 적용하고자 하니 생긴 결과물인
Mobile Clean Architecture에 대해서 정리하고자 합니다.

모바일 클린 아키텍처는 여러 원칙의 조합이며 정답은 없습니다.

위 그림은 모바일에서 일반화된 클린 아키텍처를 나타낸 것입니다.


Presentation Layer
Animation, 사용자 입력 등의 UI와 관련된 모든 처리를 담당합니다.

View : UI 표시, 사용자 입력을 처리합니다.
Activity와 Fragment도 View에 포함된다고 할 수 있죠.

Presenter : 뷰 관점에서의 비즈니스 로직을 담당합니다.
앞에서 언급된 ViewModel이라고 할 수도 있습니다.

Domain Layer
Domain은 가장 높은 수준의 계층이라고 할 수 있습니다.
Android는 Domain 계층에서 오직 순수한 Java, Kotlin 코드로만 작성되어야 한다는 특징이 있습니다. 예를 들어.. Androidx의 LiveData가 있으면 안되죠.
어느 프레임워크에서도 사용할 수 있어야 하기 때문이죠!

UseCase : Domain 관점에서의 비즈니스 로직을 담습니다.

Entity : 앱의 실질적인 데이터라고 할 수 있죠.

Translater : 데이터 계층의 Entity와 Model를 변환하는 Mapper 입니다.

Data Layer

Repository : UseCase가 필요로 하는 데이터를 저장/수정 등의 기능을 제공합니다.
-> 여기서 DataSource를 interface 형태로 참조하기 때문에 Repository 클래스에서 DataSource 객체를 갈아끼우는 형태로 Local DB, Network 출력을 자유롭게 할 수 있습니다.

DataSource : 실제 데이터의 입출력이 실행됩니다.

Entity : DataSource에서 사용하는 데이터 모델을 정의합니다.

도담도담의 구조 🏗️

도담도담은 위의 모바일 클린 아키텍처를 적용하고자 했습니다.

  • 각 계층의 역할이 잘 분리 되어있어 가독성 향상에 좋다고 생각했습니다.
    - GLASS를 개발하면서 Network 패키지에 너무 많은 역할이 부여된다고 느꼈고 이런 부분을 해결할 수 있다고 생각했습니다.
    - 가독성 뿐만 아니라 UI의 변경도 자유롭다고 생각했습니다.
  • 테스트 코드 작성에 용이하다고 생각했습니다.
    - Repostitory가 참조하는 DataSource interface도 테스트 작성에 용이하다고 생각했습니다.

이 모든 것들은 앱의 유지보수성을 높인다고 생각했습니다.
비록 러닝 커브가 높았지만 저에게는 전 버전의 코드와 선배의 너무나도 감사한 도움이 있었기에 충분히 도전할 수 있었습니다.

그러면 도담도담 구조를 뜯어보면서 Mobile Clean Architecture에 대해서 더욱 자세히 알아보겠습니다.

위 그림은 도담도담의 구조를 다이어그램으로 나타낸 것입니다.
먼저 큰 박스(presentation, domain, data, buildSrc)는 모듈을 나타냅니다.
도담도담은 총 4개의 모듈로 이루어져 있습니다. (Multi Module)

이렇게 Multi Module로 구조를 정리하면 각각의 모듈에 대한 의존성을 제어할 수 있다는 것입니다.

만약에 app 모듈에 패키지로 presentation, domain, data를 정의한다면
개발자가 data에서 presentation에 접근해 버리는 실수를 할 수도 있기 때문입니다.



위의 구조에서 domain, data가 repository를 통해서 연결되고 있습니다.
domain의 interface형인 repository를 data에서 구현하는 형태로 말이죠.

위는 의존성 역전 법칙이라고 할 수 있습니다.

고수준의 모듈이 저수준의 모듈에 의존하던 것을 뒤집어서
저수준의 모듈이 고수준의 모듈에 의존하게 하는 것을 의미합니다.

도담도담의 코드로 보자면..

급식을 받아오는 UseCase 입니다.
MealRepository를 주입 받는 것을 알 수 있습니다.


MealRepository는 이렇게 interface형입니다.


data의 MealRepository 구현체입니다.


위의 코드를 그림으로 표현하면 위와 같습니다.

이렇게 의존성을 역전한다면 추이 종속성을 막습니다.
추이 종속성은 a -> b -> c 와 같이 쭉 이어지는 종속성을 뜻하죠.
ex) MealUseCase -> MealRepository -> MealDataSource

만약의 위의 (ex)와 같이 의존성 역전을 하지 않고 추이 종속성이 생긴다면
UseCase가 DataSource에 종속성이 생기겠죠?

따라서 UseCase가 DataSource에 종속성이 생기는 것을 막아줌으로써 유지보수성에 향상이 있습니다.

후회합니다. 😭

지금 이 글을 작성하면서도 이 구조를 보며 후회하고 있습니다.

  1. Repository 패턴 적용이 이상하다.
  2. Model을 그대로 사용하는 Remote.
  3. 덕분에 entity to model만 있는 mapper.

위의 3가지 문제를 인지했습니다.
다른 문제가 있다면 꼭 댓글로 알려주세요! 너무나도 환영입니다!

1. Repository 패턴

일단 Repository 패턴이라고 한다면..

위 그림을 떠올리기 마련이죠.
Repository는 DataSource를 캡슐화합니다.
사용하는 이유는 다음과 같습니다.

  • 그림에서 ViewModel 쪽, Presentation 계층이 도메인과 연관된 모델을 가져오기 위해 필요한 DataSource가 무엇인지 알 필요가 없습니다.
  • DataSource가 바꿔치기 당해도 다른 계층은 상관이 없습니다.
  • client는 repository에 의존하기에 test하기 용이합니다.

도담도담의 Repository 패턴은 DataSource에 있습니다.
아까 전의 MealRepositoryImpl 코드를 보면 이상한 점이 하나 있었을 것입니다.
MealRepositoryImpl 전의 MealDatasource에서 MealRemote, MealCache를 받아서 내부적으로 사용하는 remote(or cache)를 은닉합니다.
저는 개발 중 도저히 쓸데없는 코드를 작성하고 있다는 생각을 지울 수 없었습니다.

이제 이 문제를 어떻게 해결할 수 있을까? 생각해 봤습니다.

Remote, Cache -> RemoteDataSource, CacheDataSource

이렇게 변경 후 기존의 DataSource의 역할을 Repository가 한다면 70% 정도 올바르게 Repository 패턴을 사용했다고 생각합니다.

그림으로 보자면..

나머지 30%인 제가 놓친 부분은 DataSource의 데이터를 그대로 전달한다는 것입니다. 아래에서 계속됩니다.

2, 3. Model.

도담도담을 테스트로 배포한 후, 생각보다 오류 사항이 많았습니다.
물론 서버에서 오는 값이 달라지기도 했습니다.
여기서 문제는 서버에서 값이 달라지면 Model이 달라졌다는 것입니다.
Model의 변경은 Presentation 전반에 영향을 끼쳤습니다.
유지보수성이 너무나도 떨어진다고 생각했습니다.

원인은 위의 Repository 패턴에서 DataSource의 데이터를 그대로 전달하는 것입니다.

여기서 저는

DataSource의 data를 Model로 변경하는 Mapper를 추가해야 했습니다..


위와 같은 구조를 가져야 한다고 생각니다.

2023. 도담 Teacher 🎓

드디어 마지막 챕터인 도담 Teacher 입니다.
앞에서 언급했듯이 도담 Teacher는 도담도담의 선생님 전용 서비스입니다.
현재 V2 버전까지 나왔으며 레거시 코드에서 계속해서 오류가 발생하여 이번 겨울 방학 리펙토링을 결정했습니다!

현재 도담 Teacher 리펙토링은 이제 막 시작했으며 아직 진행 중인 프로젝트입니다. 심지어 1차 아키텍처 설계도 이제 막 완성했으며 회의와 점검을 하면서 더욱 좋은 아키텍처를 설계할 예정입니다.

부디 이 글을 보며 좋은 피드백이 생각나신다면 댓글로 달아주세요!

도담 Teacher 1차 아키텍처.

기존의 도담도담을 개발하면서 생각한 문제를 최대한 해결하려고 했습니다.


7개의 모듈

기존의 도담도담의 구조의 data layer에 부여되는 역할이 너무 많다고 생각했습니다.
즉, Clean Architecture의 특징인 관심사 분리를 더욱 잘 실천하기 위해서 local, remote 모듈을 추가했습니다.

di 모듈을 추가했습니다.
기존의 도담도담은 presentation 모듈에 di가 있었습니다.
따라서 presentation 모듈이 di로 인해서 data 모듈을 의존하게 했습니다.
결국 Multi Module의 목적이 약간 흐려지게 되었죠..


Mapper👈

도담도담 구조 중 가장 아쉬웠던 Mapper를 추가해 줬습니다.

이렇게 Mapper를 추가하여 response를 그대로 전달하지 않게 되었습니다.
이렇게 해주면 response가 변경되어도 presentation layer에 영향을 끼치지 않게 됩니다.


DataSource, Repository

상당히 고민하고 있는 부분입니다.

위 구조는 Repository 패턴을 이상적으로 구현한 것입니다.
사실 위의 구조가 Clean Architecture적으로 적합한 구조라고 생각하며
쓸데없는 코드가 없다고 생각합니다.

하지만

위와 같은 구조를 1차로 잡은 이유는 아래와 같습니다.

  • 이미 DataSource에서 Repository 패턴의 역할을 하고 있습니다.
  • 무슨 구조가 더욱 개발에 효율적인가로 보자면 팀원 입장에서 이미 익숙한 구조가 코드를 짜는데 더욱 효율적이지 않을까 생각했습니다.

아키텍처에는 정답이 없다고 생각합니다. 각자의 프로젝트에 맞는 최선의 구조를 가져가는 것이 최고라고 생각합니다!

도담 Teacher는 MVI입니다.


Presentation Layer를 보면 MVI 패턴이라는 것을 알 수 있습니다.

기존 도담도담에서 MVVM을 사용하면서 상태 관리가 힘들다는 한계를 느꼈습니다.

도담도담에서는 외출/외박 데이터를 받아왔는데 아무값도 없을 경우, 로딩 프로그래스를 계속 보여주는 이슈도 있었습니다.

그리고 선언형 UI인 Jetpack Compose를 도입하게 되었습니다.
지금 DUI(Dodma UI)라는 디자인 시스템을 개발하고 있습니다.

Compose는 상태가 상당히 중요합니다.
Compose에서 상태가 잘못 관리될 경우에 Recomposition이 빈번하게 발생하며 UI가 갱신되지 않을 수 있습니다.

한 믿음직한 블로그에서 "MVI는 쉽게 상태 관리를 해준다." 라는 말을 들었고 Jetpack compose를 사용하는 도담 Teacher는 MVI를 적용하기로 했습니다.

정리하자면,

더욱 쉬운 상태 관리를 위해 MVI를 사용했습니다.

MVI?

저는 Compose를 공부하면서 동시에 MVI 또한 공부했습니다.
MVI는 Model - View - Intent의 약자입니다.

  • Model은 앞에서 계속 나왔던 그 Model과 같습니다.
    상태를 나타내죠.
  • View는 Activity, Fragment, Compose 등의 View를 나타냅니다.
  • Intent는 사용자 또는 앱 내 발생하는 Action을 나타냅니다.

MVI는 단방향 순환 구조입니다.
이 말을 이해하기 위해서 한 예시를 봅시다.

저희 학교 학생(user)는 도담도담(앱)에서 급식을 갱신하여 보고 싶습니다.
학생은 도담도담의 "급식 보기" 버튼을 누릅니다.(intent)
그러면 서버에서 받아온 급식(model)이 리스트(view) 형태로 보입니다.

  • intent : 급식을 데이터를 갱신하고자 하는 의도입니다.
  • model : 상태이며, 의도(intent)에 의해 업데이트 됩니다.
  • view : model이 반영됩니다.

여기서 이 구조를 그림으로 표현하면 단방향 순환 구조라는 것이 이해될 것입니다.

여기서 MVI의 한 가지 특징은 Model은 불변해야 합니다.
Intent로 인해서 Model을 생성할 때는 항상 새로운 Model 객체를 만들어야 하죠!

Model이 불변하니, 단방향 순환 구조임을 보장할 수 있습니다. 따라서 예측 가능한 상태이며 디버깅이 쉬워지는 효과를 누릴 수 있습니다.

그리고 MVI에서는 이전의 State와 Intent를 통하여 Reducer를 실행시켜 새로운 Model을 만들어냅니다.


하지만 항상 위의 순환 구조로 잘 돌아가진 않습니다.
예를 들어 Toast Message가 있습니다.
Toast는 상태를 변경할 필요가 없죠.

그래서 MVI에서 Side Effects라는 개념을 사용해서 상태 변경이 없는 이벤트를 처리합니다.

도담 Teacher의 MVI는?

도담 Teacher에서는 MVI를 쉽게 사용하기 위해서 Orbit 프레임워크를 사용했습니다.

Orbit은 몇몇 해커톤과 참여 중인 다른 프로젝트에서 이미 사용 중인 프레임워크로 참고할 수 있는 코드가 있어 쉽게 사용할 수 있다고 생각했습니다.

간단하게 급식을 보여주는 기능을 통해서 MVI를 사용해 봤습니다.

구성은 위와 같이 이뤄집니다.
먼저 Model과 SideEffect부터 만들어줬습니다.(GetMealContract)

data class GetMealState(
    val loading: Boolean = false,
    val exception: Throwable? = null,
    val meal: Meal? = null,
)

sealed class GetMealSideEffect {
    data class Toast(val message: String) : GetMealSideEffect()
}

SideEffect는 sealed class를 사용했습니다.
그리고 이 상태를 저장할 ViewModel을 만들어줬습니다.(MealViewModel)

@HiltViewModel
class MealViewModel @Inject constructor(
    private val getMealUseCase: GetMealUseCase,
) : ContainerHost<GetMealState, GetMealSideEffect>, ViewModel() {

    override val container = container<GetMealState, GetMealSideEffect>(GetMealState())

    fun getMeal(date: LocalDate) = intent {
        reduce {
            state.copy(
                loading = true
            )
        }
        getMealUseCase(date)
            .onSuccess {
                postSideEffect(GetMealSideEffect.Toast("breakfast : ${it.breakfast}"))
                reduce {
                    state.copy(
                        loading = false,
                        meal = it
                    )
                }
            }
            .onFailure { exception ->
                reduce {
                    state.copy(loading = false, exception = exception)
                }
            }
    }
}

reduce를 통해서 새로운 상태를 만들어냅니다.
그리고 MealScreen에서는 아래와 같이 사용합니다.

@Composable
fun MealScreen(
    mealViewModel: MealViewModel = hiltViewModel(),
) {
    val context = LocalContext.current

    val state = mealViewModel.collectAsState().value
    mealViewModel.collectSideEffect { handleSideEffect(context, it) }

    Column(
        modifier = Modifier.fillMaxSize()
    ) {
        Button(onClick = { mealViewModel.getMeal(LocalDate.now()) }) {
            Text(text = "SHOW!")
        }
        if (state.loading) {
            showToast(context, "로딩 중...")
        } else {
            state.meal?.let {
                MealBox(meal = it)
            }
        }
    }
}

private fun handleSideEffect(context: Context, sideEffect: GetMealSideEffect) {
    when (sideEffect) {
        is GetMealSideEffect.Toast -> showToast(context, sideEffect.message)
    }
}

이렇게 MVI에 대해서도 작성해 보았습니다!

마치며 👍

고등학생에서 취업을 준비하는 고등학생이 되어버린 2023년, 어떻게 면접에서 경험을 기반으로 답변할 수 있을까에 대해서 생각하다 보니 이 글을 쓰자고 마음을 먹었습니다.
결국 공부 내용 정리, 회고, 약간의 생각 등 정말 많은 요소가 글에 들어가게 되었습니다.

아직 초보 개발자라 모르는 부분이 정말 많습니다. 🥲
글에 "이런 부분은 잘못된 지식이에요!" 같은 것이 있다면 언제든 알려주세요!
혹은 "이런 아키텍처, 패턴을 여기에 적용하거나 뭐를 조정하면 더 좋을 거예요!" 같은 좋은 정보가 있으면 언제든 알려주세요! 환영입니다!

지금까지 도담도담 아키텍처에 대해서 작성해 보았습니다. 감사합니다. 😀

profile
응애 Android 개발자

6개의 댓글

comment-user-thumbnail
2023년 1월 29일

너무 멋지네요
지치지 말고 앞으로도 화이팅임다 🔥

1개의 답글
comment-user-thumbnail
2023년 2월 1일

화이팅 입니다

1개의 답글
comment-user-thumbnail
2023년 2월 3일

이상하게 만들어 놓고 가서 죄송합니다 :<

1개의 답글