Clean Architecture 란?
Clean Architecture는 소프트웨어 설계 원칙을 기반으로 한 아키텍쳐 스타일입니다.
로버트 C.마틴이 제안한 것으로, 코드의 각 계층을 분리하여 특정 계층이 다른 계층에 영향을 미치지 않도록 하는 것이 목표입니다.
이를 통해, 코드의 유지보수성과 확장성을 높이고 의존성을 최소화하여 안정적인 애플리케이션 개발을 가능하게 합니다.
핵심 개념은 의존성 규칙(Dependency Rule)입니다. 이 규칙에 따르면, 의존성은 반드시 바깥쪽 계층에서 안쪽 계층으로 향해야 합니다.
이를 통해 비즈니스 로직이나 애플리케이션의 핵심 기능이 외부의 UI, DB, 프레임워크 등과 분리되어 독립적으로 존재할 수 있습니다.
Android 개발에서 Clean Architecture의 필요성
애플리케이션을 개발하며, 다양한 기능이 추가되고 코드가 복잡해져 유지보수가 어려워질 수 있습니다.
하나의 Acticity나 Fragment에서 UI, 비즈니스 로직, 데이터 로직 등이 모두 섞여있을 때 작은 수정에도 큰 영향을 줄 수 있습니다.
Android에서는 외부 라이브러리나 API, DB 등과 같은 다양한 외부 의존성을 가집니다.
Clean Architecture는 의존성을 관리하는 데 있어 명확한 원칙을 제공하기 때문에, 새로운 코드를 추가하거나 수정할 때 영향을 주지 않고 시스템을 확장할 수 있습니다.
최신 안드로이드 앱 아키텍처는 유지보수성과 확장성을 높이고, 복잡한 UI를 효율적으로 관리하며, 사용자 경험을 향상시키기 위해 여러가지 설계 원칙과 기법을 통합하고 있습니다.
1. 반응형 및 계층형 아키텍처
2. 단방향 데이터 흐름(UDF)
3. 상태 홀더가 있는 UI 레이어로 UI 복잡성 관리
4. 코루틴 및 Flow
5. DI 권장
소프트웨어 설계 원칙과 Clean Architecture
Clean Architecture는 SOLID 원칙과 같은 소프트웨어 설계 원칙을 기반으로 합니다.
- SOLID 원칙
1. 단일 책임 원칙(SRP: Single Responsibility Principle)
2. 개방-폐쇄 원칙(OCP: Open/Closed Principle)
3. 리스코프 치환 원칙(LSP: Liskov Substitution Principle)
4. 인터페이스 분리 원칙(ISP: Interface Segregation Principle)
5. 의존성 역전 원칙(DIP: Dependency Inversion Principle)
이 중에서 DIP가 클린 아키텍쳐에서 가장 중요한 원칙입니다.
DIP는 고수준 모듈(비즈니스 로직 등)이 저수준 모듈(데이터베이스, 네트워크 등)에 의존해서는 안되고, 추상화(인터페이스)에 의존해야 하기 때문에, Clean Architecture의 핵심 철학과 잘 맞닿아 있습니다.
Clean Architecture의 레이어 구조
주요 레이어는 Presentation Layer, Domain Layer, Data Layer입니다.
1. Presentation Layer
- 사용자 인터페이스(UI)와 직접 상호작용하는 레이어입니다.
- Activity, ViewModel, Controller, Presenters 등이 해당됩니다.
2. Domain Layer
- 애플리케이션의 핵심 비즈니스 로직을 처리하는 레이어입니다.
- Use Case, Entities를 통해 구체적인 비즈니스 로직을 구현합니다.
3. Data Layer
- 애플리케이션이 사용하는 데이터 소스를 관리하는 역할을 합니다. 외부 리소스로부터 데이터를 가져오고 저장하는 레이어입니다.
- Room DB, DAO, Repository 등 데이터 관리 및 접근 로직을 구현합니다.
Clean Architecture를 적용한 간단한 Compose 예제 (+ Hilt)
크게 data, domain, ui(presentation)로 폴더를 나누었습니다.
data 폴더 -> datasource, repository
domain 폴더 -> model, repository, usecase
ui(presentation) -> viewmodel
이렇게 나누었습니다.
Presentation Layer - UI
MainActivity.kt
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import com.example.cleanarchitecture.ui.viewmodel.UserViewModel
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
private val viewModel : UserViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Toast.makeText(this, "My name is : ${viewModel.getUserName().name}", Toast.LENGTH_LONG).show()
setContent {
}
}
}
- 토스트로 간단히 띄우겠습니다.
위처럼 하드코딩 된 이름을 잘 가져오는 것을 볼 수 있습니다.
UserViewModel.kt
import androidx.lifecycle.ViewModel
import com.example.cleanarchitecture.domain.model.User
import com.example.cleanarchitecture.domain.usecase.UserUseCase
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class UserViewModel @Inject constructor(private val useCase: UserUseCase): ViewModel() {
fun getUserName(): User {
return useCase.getUserName()
}
}
Domain Layer
User.kt
data class User(
val name: String
)
UserRepository.kt
interface UserRepository {
fun getUserName(): User
}
UserUseCase.kt
import com.example.cleanarchitecture.domain.model.User
import com.example.cleanarchitecture.domain.repository.UserRepository
import javax.inject.Inject
class UserUseCase @Inject constructor(private val repository: UserRepository) {
fun getUserName(): User {
return repository.getUserName()
}
Data Layer
UserDataSource.kt
import com.example.cleanarchitecture.domain.model.User
import javax.inject.Inject
class UserDataSource @Inject constructor() {
fun getUserName(): User {
return User("mimisongsong")
}
}
UserRepositoryImpl.kt
import com.example.cleanarchitecture.data.datasource.UserDataSource
import com.example.cleanarchitecture.domain.model.User
import com.example.cleanarchitecture.domain.repository.UserRepository
import javax.inject.Inject
class UserRepositoryImpl @Inject constructor(private val dataSource: UserDataSource) :
UserRepository {
override fun getUserName(): User {
return dataSource.getUserName()
}
}
이렇게 간단한 예제까지 알아봤습니다.
감사합니다 !!
참고
https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
https://developer.android.com/topic/architecture?hl=ko
'안드로이드' 카테고리의 다른 글
[안드로이드] Kotlin과 JVM(Java Virtual Machine) (0) | 2024.08.29 |
---|---|
[안드로이드] Coroutine은 무엇일까? (2) | 2024.08.28 |
[안드로이드] SharedPreferences 사용법 및 저장 (0) | 2024.08.27 |
[안드로이드] Android에서 Thread는 ?? (0) | 2024.08.26 |
[안드로이드] Intent와 Bundle은 무엇일까? (0) | 2024.08.25 |