본문 바로가기
Jetpack Compose

Jetpack Compose 아키텍처 가이드 정리

by junjunjun 2023. 7. 9.
반응형

본 글은 정확하지 않을 수 있습니다. 참고용으로만 봐주시면 감사하겠습니다.

앱 아키텍처 가이드를 보고 정리하였습니다.

 

이전 글

Jetpack Compose의 Compose 이해하기

Jetpack Compose 상태 관리에 대한 정리

 

 

spring의 경우 기본적으로 controller -> service -> repository 구조로 개발이 진행된다.

jetpack compose는 어떤 구조로 개발이 진행되며, ViewModel이 사용되는 이유에 대해서 궁금했기에 알아보기로 한다.

 

모바일 앱 사용자 환경

앱의 환경 조건을 고려해 볼 때 앱 구성요소는 개별적이고 비순차적으로 실행될 수 있으며, 운영체제나 사용자가 언제든지 앱 구성요소를 제거할 수 있습니다. 

위와 같은 이유로 앱 구성요소에 애플리케이션 데이터나 상태를 저장해서는 안 되며 앱 구성요소가 서로 종속되면 안 된다.

 

다시 말해, 독립적이고 격리된 구성요소를 개발하여 각각의 역할과 책임을 잘 분리하고, 

상태와 데이터를 적절히 관리하는 방식으로 앱을 구성하는 것이 중요합니다.

 

1. 일반 아키텍처 원칙

앱을 확장하고 앱의 견고성을 높이며 앱을 더 쉽게 테스트하기 위해서는 아키텍처를 정의하는 것이 중요하다.

 

1. 관심사 분리

각 역할별로 코드를 분리하여 의존성을 최소화하는 것이 좋다.

 

2. 데이터 모델에서 UI 도출하기

데이터 모델은 앱의 데이터를 나타내며, 앱의 UI 요소 및 기타 구성요소로부터 독립되어 있다.

데이터 모델에서 UI를 도출하면 데이터와 UI 사이의 일관성을 유지할 수 있다.

 

3. 단일 소스 저장소(SSOT)

SSOT는 데이터의 소유자이며, SSOT만 데이터를 수정하거나 변경할 수 있다. SSOT는 이를 위해 불변 유형을 사용하여 데이터를 노출하며, 다른 유형이 호출할 수 있는 이벤트를 수신하거나 함수를 노출하여 데이터를 수정한다.

// 이해를 돕기 위한 코드
class UserDataSource {

    private val _userData = mutableStateOf(UserData("", 0))  // 가변 상태
    val userData: State<UserData> = _userData // 직접 수정하지 못하게 불변 유형으로 노출시킨다.

    fun updateUser(name: String, age: Int) {  // 함수를 노출하여 데이터를 수정합니다.
        _userData.value = UserData(name, age)
    }
}

 

이점

  • 특정 유형 데이터의 모든 변경사항을 한 곳으로 일원화한다.
  • 다른 유형이 조작할 수 없도록 데이터를 보호한다.
  • 데이터 변경사항을 더 쉽게 추적할 수 있도록 합니다. 따라서 버그를 발견하기가 쉬워진다.

 

4. 단방향 데이터 흐름(UDF)

UDF에서 상태는 한 방향으로만 흐릅니다. 데이터 흐름을 수정하는 이벤트는 반대 방향으로 흐른다.

상태는 상휘 컴포저블에서 하위 컴포저블로만 이동하고, 이벤트는 반대로 하위 컴포저블에서 상위 컴포저블로만 이동한다.

 

이 패턴은 데이터 일관성을 강화하고, 오류가 발생할 확률을 줄여 주며, 디버그 하기 쉽고, SSOT 패턴의 모든 이점을 제공한다.

 

 

2. 권장 앱 아키텍처

일반적인 아키텍처 원칙에 떠라 애플리케이션에는 최소한 다음 두 가지 레이어가 포함되어야 한다.

  • 화면에 애플리케이션 데이터를 표시하는 UI 레이어
  • 앱의 비즈니스 로직을 포함하고 애플리케이션 데이터를 노출하는 데이터 레이어

추가적으로 UI와 데이터 레이어 사이에 도메인 레이어를 추가할 수도 있다.(선택)

일반적인 앱 아키텍처

 

 

UI 레이어(또는 프레젠테이션 레이어)

UI 레이어의 역할은 화면에 애플리케이션 데이터를 표시하는 것이다.

이벤트로 인해 데이터가 변경될 때마다 UI가 업데이트되어야 한다.

 

UI 레이어는 다음 두 가지로 구성된다.

  1. 화면에 데이터를 렌더링 하는 UI 요소(ex : 구성 가능한 함수)
  2. 데이터를 보유하고 이를 UI에 노출하며 로직을 처리하는 상태 홀더(ex : ViewModel)

UI 레이어의 역할

 

데이터 레이어

앱의 데이터 레이어에는 비즈니스 로직이 포함되어 있다. 앱의 데이터 생성, 저장, 변경 방식을 결정하는 규칙으로 구성된다.

데이터 레이어는 0개부터 여러 개의 데이터 소스를 가질 수 있는 저장소로 구성됩니다.

앱에서 처리하는 다양한 유형의 데이터별로 저장소 클래스를 만들어야 한다. ex) MoviesRepository

데이터 레이어의 역할

 

도메인 레이어

도메인 레이어는 UI 레이어와 데이터 레이어 사이에 있는 선택적 레이어이다.

도메인 레이어는 복잡한 비즈니스 로직이나 여러 ViewModel에서 재사용되는 간단한 비즈니스 로직의 캡슐화를 담당한다.

 

 

3. UI 레이어 자세히

3.1 UI 레이어 정의

데이터 레이어에서 가져온 상태를 시각적으로 나타낸다. 하지만 데이터 레이어에서 가져오는 데이터 형식과 화면에 표시하는 데이터 형식은 다르다.

 

따라서 데이터 레이어에서 가져온 데이터를 UI가 표시할 수 있는 형식으로 변환한 후에 표시한다.

 

실행단계

  1. 앱 데이터를 UI에서 쉽게 렌더링 할 수 있는 데이터로 변환한다.
  2. UI 렌더링 가능 데이터를 사용하고 사용자에게 표시할 UI 요소로 변환한다.
  3. 이러한 UI 요소의 사용자 이벤트를 사용하고 이벤트 결과에 따라 UI 데이터를 반영한다.
  4. 1~3단계를 반복한다.

 

 

UI 상태 정의

UI 요소 + UI 상태 = UI

  • UI 요소는 구성가능한 함수를 말한다. ex) 화면에 보이는 버튼, 텍스트
  • UI 상태는 앱에서 사용자에게 보이는 데이터를 말한다. ex) 기사 제목, 내용
// UI 상태 정의
// 보통 이런식으로 필요한 정보를 정의된 데이터 클래스에 캡슐화한다.
data class NewsUiState(
    val isSignedIn: Boolean = false,
    val isPremium: Boolean = false,
    val newsItems: List<NewsItemUiState> = listOf(),
    val userMessages: List<Message> = listOf()
)

...
val uiState: StateFlow<NewsUiState> = … // 이런식으로 사용

 

만약 NewsUiState에서 isSignedIn을 true로 변경하고 싶다면 속성을 변경한 새로운 객체를 생성해야 된다.

왜냐하면 UI 상태 정의는 불변성이기 때문이다. 불변성이기 때문에 앱 상태를 보장할 수 있다.

 

 

단방향 데이터 흐름으로 상태 관리

UI 상태를 생성하는 역할을 담당하고 생성 작업에 필요한 로직을 포함하는 클래스를 상태 홀더라고 합니다.

전체 화면이나 탐색 대상의 경우 일반적인 구현은 ViewModel의 인스턴스이지만 애플리케이션의 요구사항에 따라 간단한 클래스로도 충분할 수 있습니다.

 

ViewModel 유형은 데이터 레이어에 액세스 할 권한이 있는 화면 수준 UI 상태를 관리하는 데 권장되는 구현입니다

 

UDF의 작동 방식

  • ViewModel이 UI에 사용될 상태(UI state)를 보유하며, UI 상태는 ViewModel에 의해 변환된 애플리케이션 데이터이다.
  • UI 요소가 ViewModel에게 사용자 이벤트를 알린다.
  • ViewModel이 사용자 작업을 처리하고 상태를 업데이트한다.
  • 업데이트된 상태가 렌더링 할 UI에 다시 제공된다.
  • 상태 변경을 야기하는 모든 이벤트에 위의 작업이 반복된다.

쉽게 말하자면, UI에서 발생한 이벤트를 상태홀더(viewmodel)에게 알린 뒤 data 계층에서 데이터를 수정하고 이를 다시 상태홀더에게 알려줘서 상태(ui state)를 업데이트한다.

 

 

예시 코드

// viewModel 예시 코드
class NewsViewModel(
    private val repository: NewsRepository,  // data 레이어
    ...
) : ViewModel() {

   var uiState by mutableStateOf(NewsUiState())  // UI 상태 정의, 내부적으로만 수정 가능
        private set

    private var fetchJob: Job? = null

	// 상태를 변경시키는 메서드는 노출시킨다.
    fun fetchArticles(category: String) {
        fetchJob?.cancel()
        fetchJob = viewModelScope.launch {  // 비동기 작업 수행
            try {
                val newsItems = repository.newsItemsForCategory(category)
                uiState = uiState.copy(newsItems = newsItems) // 상태는 불변성이기에 업데이트된 새로운 상태 생성
            } catch (ioe: IOException) {
                // Handle the error and notify the UI when appropriate.
                val messages = getMessagesFromThrowable(ioe)
                uiState = uiState.copy(userMessages = messages) // 상태는 불변성이기에 업데이트된 새로운 상태 생성
            }
        }
    }
}

...

// 컴포저블에서 viewModel 사용 방법
@Composable
fun LatestNewsScreen(
    viewModel: NewsViewModel = viewModel()
) {
    // Show UI elements based on the viewModel.uiState
}

 

 

3.2 UI 이벤트

UI 이벤트는 UI 레이어에서 UI 또는 ViewModel로 처리해야 하는 작업이다. ex) 버튼 클릭 이벤트

 

ViewModel에서는 일반적으로 특정 사용자 이벤트의 비즈니스 로직을 처리한다.

UI에서는 직접 처리할 수 있는 UI 동작 로직을 처리한다.

 

UI 이벤트 결정 트리

 

UI에서 발생된 이벤트에 대한 예시 코드

@Composable
fun LatestNewsScreen(viewModel: LatestNewsViewModel = viewModel()) {

    var expanded by remember { mutableStateOf(false) }

    Column {
        Text("Some text")
        if (expanded) {
            Text("More details")
        }

        Button(
          // 버튼 클릭스 UI 요소만 변경됨
          // UI 에서 이벤트 처리
          onClick = { expanded = !expanded }  
        ) {
          val expandText = if (expanded) "Collapse" else "Expand"
          Text("$expandText details")
        }

        // 버튼 클릭시 비즈니스 로직이 실행되야 함
        // viewModel에서 이벤트 처리
        Button(onClick = { viewModel.refreshNews() }) {
            Text("Refresh data")
        }
    }
}

 

viewModel에서 발생된 이벤트에 대한 예시 코드

참고로 viewModel에서 발생된 이벤트는 항상 UI 상태 업데이트로 이어진다.

data class LoginUiState(
    val isLoading: Boolean = false,
    val errorMessage: String? = null,
    val isUserLoggedIn: Boolean = false
)

class LoginViewModel : ViewModel() {
    var uiState by mutableStateOf(LoginUiState())
        private set
    /* ... */
}

@Composable
fun LoginScreen(
    viewModel: LoginViewModel = viewModel(),
    onUserLogIn: () -> Unit
) {
    val currentOnUserLogIn by rememberUpdatedState(onUserLogIn)

    // viewModel 이벤트에 반응하는 코드이다.
    // uiState가 변경될 때마다 사용자가 로그인되어 있는지 확인한다.
    LaunchedEffect(viewModel.uiState)  {
        if (viewModel.uiState.isUserLoggedIn) {
            currentOnUserLogIn()
        }
    }

    ...
}

 

 

4. 데이터 영역

4.1 데이터 영역 정보

데이터 영역에는 애플리케이션 데이터 및 비즈니스 로직이 포함된다.

이렇게 관심사를 분리하면 데이터 영역을 여러 화면에서 사용하고, 앱의 여러 부분 간에 정보를 공유하고, 단위 테스트를 위해 UI 외부에서 비즈니스 로직을 재현할 수 있다.

 

앱에서 처리하는 다양한 유형의 데이터별로 저장소 클래스를 만들어야 합니다. 예를 들어 영화 관련 데이터에는 MoviesRepository 클래스를 만들어야 한다.

 

데이터 영역의 진입점은 항상 저장소 클래스여야 한다. 다른 레이어에서는 데이터 소스를 직접 접근할 수 없다.

 

데이터 소스 클래스는 애플리케이션과 해당 데이터 소스 간의 연결 역할을 합니다. 

즉, 애플리케이션은 데이터 소스 클래스를 통해 데이터를 가져오고 저장하며, 데이터 소스 클래스는 이를 실제 데이터 소스(파일, 네트워크, 로컬 데이터베이스 등)와 연결하여 데이터 작업을 수행합니다.

// 데이터 레이어에 대한 예시 코드

// 네트워크 연결 데이터 소스 클래스
class NewsRemoteDataSource(
  private val newsApi: NewsApi,
  private val ioDispatcher: CoroutineDispatcher
) {

	// 네트워크에서 최신 뉴스를 가져오고 결과를 반환합니다.
    suspend fun fetchLatestNews(): List<ArticleHeadline> =
        withContext(ioDispatcher) {
            newsApi.fetchLatestNews()
        }
    }
}

// 저장소 클래스
class NewsRepository(
    private val newsRemoteDataSource: NewsRemoteDataSource
) {
    suspend fun fetchLatestNews(): List<ArticleHeadline> =
        newsRemoteDataSource.fetchLatestNews()
}

 

 

결론

  • 아키텍처 원칙으로 관심사 분리, 데이터 모델에서 UI 도출하기, 단일 소스 저장소, 단방향 데이터 흐름 이 있다.
  • 권장 앱 아키텍처 구조는 Ul Layer -> Domain Layer(옵션) -> Data Layer이다.
  • UI 레이어는 화면에 데이터를 렌더링 하는 UI 요소와 데이터를 보유하고 이를 UI에 노출시키는 상태 홀더(ViewModel)로 구성된다.
  • 데이터 레이어는 저장소Data Source로 구성된다.
  • UI는 UI 요소 + UI 상태이다. UI 상태는 보통 __UIState이름으로 데이터 클래스 형태로 사용되며 불변성이다.
  • UI 상태를 관리하는 역할을 상태 홀더(ViewModel)가 한다.
  • 현재까지 이해한 전체적인 아키텍처 흐름은 UI -> ViewModel(uiState보유) -> Repository -> DataSource이다.

 

데이터 영역을 포함한 개발을 해본 적이 없기에 문서를 봐도 감이 잘 오지 않는다. 추후 제대로 된 정리가 필요해 보인다.

 

반응형

댓글