공식 문서에서 권장하는 앱 아키텍처는 아래와 같습니다
우선 각 계층에 대해 알아보고 권장 앱 아키텍처의 주요 목표에 대해 알아보겠습니다
1. Presentation Layer / UI Layer (사용자 인터페이스 관련 책임)
- Fragment, Activity
- ViewModel : UI 관련 데이터를 관리, UI와 비즈니스 로직(도메인 레이어) 간의 연결 역할
2. Domain Layer (비즈니스 로직 관련 책임)
- UseCase: 도메인 레이어의 핵심 구성 요소로 비즈니스 로직을 수행하는 단위로,
데이터 레이어에서 데이터를 가져와 비즈니스 로직을 처리한 후, 결과를 UI 레이어에 전달합니다.
확장성과 재사용성을 고려해서 주로 하나의 비즈니스 로직당 하나의 use case를 두는 것이 권장됩니다
3. Data Layer (데이터 저장 관리)
- Data Source : api, DB에 접근해 데이터의 저장 및 조회를 담당
- Repository : 도메인 레이어가 데이터에 접근할 수 있는 인터페이스를 제공, 데이터 소스의 세부 구현을 숨깁니다. 도메인 레이어가 데이터 소스의 변경에 영향을 받지 않도록 합니다
정리하자면 , layer는 단방향 참조입니다 ui layer -> domain layer -> data layer
- Presentation Layer : UI와 관련된 코드
- Domain Layer : 비즈니스 로직과 규칙
- Data Layer : 데이터 소스와 관련된 코드 (API, 데이터베이스 등)
- Presentation Layer는 Domain Layer를 참조합니다. 즉, Presentation Layer는 Domain Layer의 UseCase를 호출하여 비즈니스 로직을 수행합니다.
- Domain Layer는 Data Layer를 참조합니다. Domain Layer의 UseCase는 Data Layer의 Repository를 호출하여 데이터를 가져옵니다.
- Data Layer는 Domain Layer를 참조하지 않습니다. 즉, Data Layer는 Domain Layer의 UseCase나 Presentation Layer에 대해 알지 못합니다.
이렇게 3개의 layer로 나눈 아키텍처에는 중요한 목표가 있습니다
관심사 분리
- 레이어간 명확한 책임 분리
유지보수성, 유연성
- 레이어들이 독립적으로 설계되어, 특정 레이어를 수정할 때 다른 부분에 영향 X
테스트 용이성
- 각 레이어가 독립적으로 설계되어 있어 유닛 테스트와 통합 테스트를 쉽게 수행
재사용성
- 레이어들이 독립적으로 설계되어, 특정 레이어를 수정할 때 다른 부분에 영향 X
의존성 역전
- 상위 레이어가 하위 레이어에 의존하지 않음 (ex. UI Layer가 Domain Layer를 의존하지 않음)
간단하게 코드로 예제를 보자면 아래와 같습니다
// 도메인 모델
data class UserDomainModel(
val id: String,
val name: String,
val email: String
)
// 데이터 모델
data class UserDataModel(
val id: String,
val name: String,
val email: String
)
// 데이터 소스 인터페이스
interface UserDataSource {
fun fetchUser(userId: String): UserDataModel
}
// 데이터 소스 구현 클래스
class UserDataSourceImpl : UserDataSource {
override fun fetchUser(userId: String): UserDataModel {
// 실제 데이터베이스나 API 호출 로직을 구현
return UserDataModel(id = userId, name = "Kong Droid", email = "Kong Droid@kong.droid.com")
}
}
// 매퍼
매퍼는 데이터 모델을 도메인 모델로 변환을 담당하는 클래스입니다
object UserMapper {
fun mapToDomain(userData: UserDataModel): UserDomainModel {
return UserDomainModel(
id = userData.id,
name = userData.name,
email = userData.email
)
}
}
// 레포지토리 인터페이스
interface UserRepository {
fun getUser(userId: String): UserDomainModel
}
// 레포지토리 구현 클래스
class UserRepositoryImpl(private val dataSource: UserDataSource) : UserRepository {
override fun getUser(userId: String): UserDomainModel {
val userData = dataSource.fetchUser(userId)
return UserMapper.mapToDomain(userData)
}
}
// Use Case 정의
class GetUserUseCase(private val userRepository: UserRepository) {
fun execute(userId: String): UserDomainModel {
return userRepository.getUser(userId)
}
}
// Use Case 사용 예시
fun main() {
// 데이터 소스와 레포지토리 구현체 생성
val userDataSource: UserDataSource = UserDataSourceImpl()
val userRepository: UserRepository = UserRepositoryImpl(userDataSource)
// Use Case 생성
val getUserUseCase = GetUserUseCase(userRepository)
// 사용자 가져오기
val userId = "12345"
val user = getUserUseCase.execute(userId)
// 결과 출력
println("User: $user")
}
viewModel을 사용한다면 아래와 같겠습니다
class UserViewModel(private val getUserUseCase: GetUserUseCase) : ViewModel() {
private val _user = MutableLiveData<User>()
val user: LiveData<User> get() = _user
fun fetchUser(userId: String) {
val fetchedUser = getUserUseCase.execute(userId)
_user.value = fetchedUser
}
}
간단한 예제라서 di 적용은 제외했습니다