AAC 란?
AAC는 Android Architecture Components의 약자로,
테스트와 유지보수가 쉬운 앱을 디자인할 수 있도록 돕는 라이브러리의 모음이다.
쉽게 말해서 라이브러리 모음이라고 할 수 있다.
Google I/O 2017에서 새로운 라이브러리를 AAC로 묶어서 발표를 하여 AAC라는 것이 사용되게 되었고,
Google I/O 2018에서 Android Jetpack을 발표할 때는 Jetpack의 구성요소 중 하나로 AAC가 들어가 있다.
안드로이드 아키텍처 (AAC) 원칙
관심사 분리
코드를 작성할 때 Activity 혹은 Fragment와 같은 UI 기반의 클래스는 UI 및 OS 상호작용을 처리하는 로직만 포함해야 한다. 이는 UI 클래스를 최대한 가볍게 유지하여 Lifecycle 관련 문제를 피하기 위함이다.
UI 클래스는 무언가를 소유하는 것이 아닌 OS와 앱 사이의 계약을 나타내도록 이어주는 클래스일 뿐이며, 따라서 OS는 메모리 부족과 같은 특정 상황이 발생하면 언제든지 UI 클래스를 제거할 수 있다.
요약하자면, UI 클래스로부터 UI, OS 상호작용을 제외한 다른 로직을 분리하여 UI 클래스에 대한 의존성을 최소화하는 것이 앱 관리 측면에서 좋다.
모델에서 UI 만들기
UI는 Model에서 만들어져야 합니다. Model은 앱의 데이터 처리를 담당하는 컴포넌트로, 앱의 View 객체 및 앱 컴포넌트와 독립되어 있으므로 앱의 Lifecycle에 영향을 받지 않는다.
Model은 가급적 지속적인 Model을 사용하는 것이 좋은데, 이는 지속 Model을 사용하면 OS에서 리소스 확보를 위해 앱을 제거해도 사용자 데이터가 삭제되지 않고 네트워크에 문제가 있어도 앱이 계속 작동하게 할 수 있기 때문이다.
앱 아키텍처 가이드 | Android 개발자 | Android Developers
앱 아키텍처 가이드 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 이 가이드에는 고품질의 강력한 앱을 빌드하기 위한 권장사항 및 권장 아키텍처가 포함
developer.android.com
AAC 구성 요소
Google I/O 2018의 발표 자료를 보면 다음과 같은 이미지를 확인할 수 있는데, 여기서 Architecture 부분이 AAC라고 볼 수 있다.
New로 붙어있는 것들이 Jetpack에서 추가된 AAC이고, 그렇지 않은 부분이 2017년도 Google I/O에서 발표된 AAC라고 생각하면 된다.
- Lifecycles(Easy handling lifecycles)
- LiveData(Lifecycle aware observable)
- ViewModel(Managing data in a lifecycle)
- Room(object Mapping for SQLite)
- Paging(Gradually loading information)
- Databinding
- Navigation
- WorkManager
각각에 대해 자세히 알아보자.
1. Lifecycles
Lifecycles는 생명주기 모니터링을 돕는다. 크게 Lifecycle Owner,Lifecycle Observer 로 구성되어있다.
Lifecycle Owner
Activity,Fragment에서 생명주기를 분리하여 Lifecycle 객체에 담는다.
Lifecycle 객체를 통해 다른 곳에서 해당 화면의 생명주기를 모니터링 할 수 있는데
자신의 생명주기를 담은 Lifecycle 객체가 Lifecycle Owner이다.
// 생명주기를 Lifecycle로 담을 수 있도록 LifecycleActivity를 상속받는다.
class MainActivity extends LifecycleActivity {
private MyLocationListener myLocationListener;
public void onCreate(Bundle savedInstanceState) {
// getLifecycle()로 생명주기가 담긴 Lifecycle 객체를 받는다.
myLocationListener = new MyLocationListener(this, getLifecycle(), location -> {
// update UI
});
}
}
class MyLocationListener {
private boolean enabled = false;
private Lifecycle lifecycle;
public MyLocationListener(Context context, Lifecycle lifecycle, Callback callback) {
}
public void enable() {
enabled = true;
// Lifecycle.getCurrentState()로 생명주기 모니터링이 가능하다.
if (lifecycle.getCurrentState().isAtLeast(STARTED)) {
// connect if not connected
}
}
}
Lifecycle Observer
생명주기를 Wrapping한 Lifecycle Owner 객체를 통해 화면 밖에서도 모니터링이 가능하지만, 생명주기에 따른 동작은 여전히 화면에서만 정의할 수 있다. 화면 밖에서도 생명주기에 따른 동작을 정의하기 위해서는 원하는 클래스에 LifecycleObserver 인터페이스를 구현하고 넘겨받은 Lifecycle Owner객체에 구현한 LifecycleObserver를 등록해야 한다. Lifecycle Observer를 구현한 클래스는 onResume()등의 생명주기 메소드를 정의할 수 있다. 이 메소드들은 등록한 Lifecycle Owner가 해당 생명주기 상태가 되면 자동으로 수행되면서, 객체가 화면과 동일한 생명주기를 가진 것처럼 행동한다.
class MyLocationListener implements LifecycleObserver {
public MyLocationListener(Context context, Lifecycle lifecycle, Callback callback) {
// Lifecycle에 생명주기 메소드를 정의한 LifecycleObserver를 등록한다
lifecycle.addObserver(this);
}
// Lifecycle Owner의 onStart()에 동작할 메소드
@OnLifecycleEvent(Lifecycle.Event.ON_START)
void start() {
// Do something
}
// Lifecycle Owner의 onStop()에 동작할 메소드
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
void stop() {
// Do something
}
}
Lifecycles를 통해 우리는 화면 밖에서 화면의 생명주기를 모니터링 하고, 동작을 정의할 수 있다.
이는 더 직관적인 생명주기 프로그래밍을 가능하게 한다.
2. LiveData
LiveData는 Observable 형태로 사용하며, 안드로이드 Lifecycle에 따라 데이터를 관리한다. Activity, Fragment의 라이프 사이클을 따
르기에 활동에 대한 처리를 알아서 관리해준다.
LiveData는 식별 가능한 데이터 홀더 클래스로 다른 앱 컴포넌트의 Lifecycle을 인식하며, 이를 통해 활성 상태에 있는 앱 컴포넌트 옵저버에게만 업데이트 정보를 알린다.
LiveData를 사용하면 다음과 같은 이점이 있다.
- UI와 데이터 상태의 일치 보장: LiveData는 Observer Pattern을 따르며 Lifecycle 상태가 변경될 때마다 Observer 객체에 알린다. 또 앱 데이터의 변경이 발생할 때마다 관찰자에게 알려 UI를 업데이트할 수 있도록 한다.
- 메모리 누수 없음: Observer는 Lifecycle 객체에 결합되어 있으며 연결된 객체의 Lifecycle이 끝나면 자동으로 삭제된다.
- 중지된 활동으로 인한 비정상 종료 없음: 활동이 백 스택에 있을 때를 비롯하여 Observer가 비활성 상태에 있으면 어떤 LiveData 이벤트도 받지 않는다.
- Lifecycle을 더 이상 수동으로 처리하지 않음: UI 컴포넌트는 관련 데이터를 관찰하기만 할 뿐 관찰을 중지하거나 다시 시작하지 않으며, LiveData가 이를 자동으로 관리한다.
- 최신 데이터 유지: 컴포넌트가 비활성화되면 다시 활성화될 때 최신 데이터를 수신한다.
- 적절한 구성 변경: 기기 회전과 같은 구성 변경으로 인해 액티비티나 프래그먼트가 다시 생성되면 최신 데이터를 즉시 받게 된다.
- 리소스 공유: 앱에서 시스템 서비스를 공유할 수 있도록 싱글톤 패턴을 사용하는 LiveData 객체를 확장하여 시스템 서비스를 래핑할 수 있다.
LiveData 객체는 다음과 같은 순서로 사용된다.
- ViewModel 클래스 내에서 특정 유형의 데이터를 보유할 LiveData의 인스턴스를 만든다.
- onChanged() 메서드를 정의하는 Observer 객체를 UI Controller에 만듭니다. onChanged() 메서드는 LiveData 객체가 보유한 데이터가 변경될 경우 발생하는 작업을 제어한다.
- observe() 메서드를 사용하여 LiveData 객체에 Observer 객체를 연결한다.
- LiveData 객체를 업데이트하는 경우 MutableLiveData 클래스는 setValue(T) 또는 postValue(T) 메서드로 LiveData 객체에 저장된 값을 수정한다.
// LiveData를 상속받는다. Location 타입의 데이터를 Wrapping 하였다.
class MyLocationListener extends LiveData<Location> {
public MyLocationListener(Context context) {
}
// 1개 이상의 active observer
@Override
protected void onActive() {
// Do something
}
// 0개의 active observer
@Override
protected void onInactive() {
// Do something
}
}
LiveData는 Active observer 갯수에 따라 onActive(), onInactive()가 불린다. Observer는 LiveData.observe() 메소드로 등록하며, 이 메소드는 데이터 변경을 구독합니다. Active observer라 함은 생명주기가 최소 Resumed 또는 Started에 있는 Observer를 뜻한다. Observer가 생명주기에 따라 active, inactive 상태를 판단하기 위해 observe()를 호출할 때 Lifecycle을 넘긴다. LiveData는 이렇게 Active observer 갯수로 생명주기를 간접적으로 인지한다.
class MainActivity extends LifecycleActivity {
public void onCreate(Bundle savedInstanceState) {
LiveData<Location> myLocationListener = new MyLocationListener();
// observe()로 데이터 변경 이밴트를 구독한다.
// Active, Inactive를 판단하기 위해 observe() 할 때 Lifecycle를 넘긴다.
myLocationListener.observe(this, location -> {
// Data가 변경되면 동작할 콜백을 등록한다.
});
}
}
LiveData는 observe()에서 넘어온 Lifecycle로 생명주기를 모니터링하고, 함께 받은 콜백으로 데이터 변경 이벤트를 구독한다. 콜백은 LiveData.setValue()로 데이터를 변경하면 호출된다.
class MyLocationListener extends LiveData<Location> {
private LocationManager locationManager;
private LocationListener listener = new LocationListener() {
@Override
public void onLocationChanged(Location location) {
// setValue()로 데이터를 변경하고 구독하는 Observer 들에게 이벤트를 전달한다.
setValue(location);
}
};
}
LiveData는 이렇게 데이터 모델을 Wrapping해서 생명주기와 데이터 변경을 자연스럽게 모델 스스로 인지할 수 있도록 한다.
LiveData 개요 | Android 개발자 | Android Developers
LiveData를 사용하여 수명 주기를 인식하는 방식으로 데이터를 처리합니다.
developer.android.com
3. ViewModel
수명주기를 고려하여 UI관련 데이터를 저장하고 관리하도록 설계되었습니다. ViewModel 클래스를 사용하면 화면전환과 같이 구성을 변경할 때도 데이터를 보존할 수 있다.
- AAC ViewModel이 데이터의 보존이 가능한 이유로는 ViewModel이 Activity의 경우 LifecycleEventObserver()로 Fragment에서는 FragmentStateManager로 View의 Lifecycle을 관찰하다 View가 종료되었을 때 Clear()를 하기때문에 종료가 되지 전까지 데이터가 보존되는 것이다.
이 때 AAC에서 말하는 ViewModel과, MVVM 패턴에서의 ViewModel은 ViewModel이라는 이름만 같을 뿐, 전혀 다른 것이다.
필자는 이에 대해 헷갈렸던 적이 있는데 이 내용에 대해서는 다른 포스팅으로 좀 더 자세하게 다뤄보겠다.
ViewModel은 앱의 Lifecycle을 고려하여 UI 관련 데이터를 저장하고 관리하는 컴포넌트이다.
UI Controller로부터 UI 관련 데이터 저장 및 관리를 분리하여 ViewModel이 담당하도록 하면 다음과 같은 문제를 해결할 수 있다.
- 안드로이드 프레임워크는 특정 작업이나 완전히 통제할 수 없는 기기 이벤트에 대한 응답으로 UI Controller를 제거하거나 다시 만들 수 있는데, 이런 경우 UI Controller에 저장된 모든 일시적인 UI 관련 데이터가 삭제된다. 단순한 데이터의 경우 onSaveInstanceState() 메서드를 사용하여 복구할 수 있지만 대용량의 데이터의 경우엔 불가능하다.
- UI Controller에서 데이터를 위한 비동기 호출을 한다면 메모리 누수 가능성을 방지하기 위한 많은 유지 관리가 필요하며, 위에서와 같이 데이터를 복귀해야 하는 경우 비동기 호출을 다시해야 해서 리소스가 낭비된다.
- UI Controller에서 DB나 네트워크로부터 데이터를 로드하도록 하면 다른 클래스로 작업이 위임되지 않고 단일 클래스가 혼자서 앱의 모든 작업을 처리하려고 할 수 있다. 이 경우 테스트가 훨씬 더 어려워진다.
ViewModel은 ViewModel 클래스를 상속받아 구현한다.
class MyViewModel : ViewModel() {
/* by lazy: val 데이터의 늦은 초기화 방법 */
private val users: MutableLiveData<List<User>> by lazy {
/* .also: scope function */
MutableLiveDate().also {
loadUsers()
}
}
fun getUsers(): LiveData<List<User>> {
return users
}
private fun loadUsers() {
// Do an asynchronous operation to fetch users.
}
}
UI 클래스에서는 다음과 같은 방법으로 ViewModel에 접근할 수 있다.
class MyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
/* by viewModels(): model의 getValue/setValue 동작을
viewModels의 getValue/setValue의 동작에 위임 */
val model: MyViewModel by viewModels()
model.getUsers().observe(this, Observer<List<User>>{ users ->
// update UI
})
}
}
ViewModel 객체는 뷰 또는 LifecycleOwners보다 오래 지속되도록 설계되었으며, ViewModel을 가져올 때 ViewModelProvider에 전달되는 LifecycleOwners에 따라 Lifecycle이 달라진다. 만약 액티비티가 전달되었다면 액티비티가 종료될 때까지, 프래그먼트가 전달되었다면 프래그먼트가 분리될 때까지 ViewModel은 메모리에 남아 있다.
4. Room
안드로이드는 로컬디비로 SQLite를 지원한다. Room 라이브러리는 SQlite에 추상화 레이어를 제공하여 원활한 DB 액세스를 지원하고 SQLite를 완벽히 활용할 수 있게 하는 라이브러리이다. Room을 사용하여 사용구 코드를 피하고 SQLite 테이블 데이터를 자바 객체로 쉽게 변환할 수 있다. Room은 SQLite 문의 컴파일 시간 확인을 제공하며 RxJavam, Flowable, LiveData, Observable을 반환할 수 있다.
Room 라이브러리를 사용하면 앱을 실행하는 기기에서 앱 데이터의 캐시를 만들 수 있고, 이 캐시를 통해 사용자는 인터넷 연결 여부와 관계없이 앱에 있는 주요 정보를 일관된 형태로 볼 수 있다.
구성요소엔 database( 데이터 베이스 홀더를 포함하고 데이터에 연결하기 위한 역할을 함), entity (데이터 베이스 내의 테이블), DAO(Data access object, 데이터 베이스에 접근할 때 사용되는 메소드를 포함)가 있다.
애플리케이션은 room 데이터 베이스를 사용해 DAO를 가져온다. 이후 DAO를 사용해 데이터 베이스에서 Entity를 가져오고 변경사항을 데이터베이스에 다시 저장한다.
Room의 구성요소
- 데이터베이스 : 데이터베이스는 앱에 저장되어 있는 로컬 데이터에 대한 액세스 포인트를 제공해주는 역할
- DAO(Data Access Object) : DAO는 앱에서 데이터베이스의 데이터를 추가, 삭제, 업데이트 작업을 할 수 있는 메소드를 제공해주는 역할, 그 외에도 다양한 쿼리 사용가능
- Entity : 데이터베이스 내에 존재하는 테이블을 가리킨다.
5. Paging
페이징 라이브러리를 사용하면 로컬 저장소에서나 네트워크 를 통해 대규모 데이터세트의 데이터 페이지를 로드하고 표시할 수 있다.
이 방식을 사용하면 앱에서 네트워크 대역폭과 시스템 리소스를 모두 더 효율적으로 사용할 수 있다.
Paging은 다음 3가지 작업이 필요하다.
- 데이터를 Page 단위로 가져오는 쿼리
- 데이터를 특정 기준으로 Page 나누기
- 중복 아이템 검사
하지만 3가지를 구현하는 것은 여러모로 귀찮은 일이 많다. Paging Library는 이것을 쉽게 구현하도록 돕는다. Paging Library는 3가지로 구성되어 있다.
- DataSource — Local 또는 Network에서 데이터를 가져오는 쿼리를 담고 있다.
- PagedList — Page 속성과 DataSource를 들고 있습니다. Page 속성에 맞추어 DataSource로 맞는 범위의 데이터를 불러온다.
- PagedListAdapter — PagedList를 UI에 보여주고 아이템 중복을 검사합니다. 다음 Page에 도달하면 데이터를 PagedList에게 요청한다.
3가지를 어떻게 구현, 연결해 Paging을 만드는지 알아보자.
(1) DataSource
DataSource는 Local 또는 Network에서 Page 단위로 데이터를 가져옵니다. PageKeyedDataSource, ItemKeyedDataSource, PositionalDataSource 3가지가 있으며, 키 속성에 따라 맞는 클래스를 상속받아 쿼리를 구현해야 합니다. Room을 사용하면 반환 타입을 DataSource를 하여 PositionalDataSource를 자동으로 생성할 수 있습니다.
// 반환 타입을 DataSource로 하면 자동으로 PositionalDataSource를 생성한다.
@Query("select * from users WHERE age > :age order by name DESC, id ASC")
DataSource.Factory<Integer, User> usersOlderThan(int age);
(2) PagedList
LivePagedListBuilder에 Page 속성과 DataSource를 정의하고 빌드하면, LiveData<PagedList<Item>> 타입을 반환합니다. PagedList는 Page 속성과 DataSource를 들고있고, LiveData로 Wrapping 된 PagedList는 실시간 데이터 모니터링이 가능합니다.
LiveData<PagedList<Item>> pagedItems =
LivePagedListBuilder(myDataSource, /* page size */ 50)
.setFetchExecutor(myNetworkExecutor)
.build();
PagedList는 PagedListAdapter가 데이터를 요청하면 자신의 속성과 DataSource를 참조하여 적절한 범위의 Page 데이터를 불러옵니다.
(3) PagedListAdapter
PagedListAdapter는 PagedList를 들고있고, 다음 데이터가 필요하면 PagedList에 요청합니다. 또한 불러온 데이터의 중복을 검사합니다. Paging은 Page가 밀리는 등 여러 경우에 아이템이 밀려 중복이 발생할 수 있습니다. DiffUtil에 기준을 정의해 PagedListAdapter의 Consturstor에 넘겨주면, 기준에 걸리는 중복 아이템은 UI에 보여주지 않습니다.
// DiffUtil 구현. PagedListAdapter Constructor에 넘겨준다.
public static final DiffUtil.ItemCallback<User> DIFF_CALLBACK =
new DiffUtil.ItemCallback<User>() {
@Override
public boolean areItemsTheSame(@NonNull User oldUser, @NonNull User newUser) {
return oldUser.getId() == newUser.getId();
}
@Override
public boolean areContentsTheSame(@NonNull User oldUser, @NonNull User newUser) {
return oldUser.equals(newUser);
}
}
(4) Paging Library 종합
위 3개를 모두 사용해 Paging Libary를 실제로 활용한 예시코드는 다음과 같다.
@Dao
interface UserDao {
// The Integer type parameter tells Room to use a PositionalDataSource
// object, with position-based loading under the hood.
@Query("SELECT * FROM user ORDER BY lastName ASC")
public abstract DataSource.Factory<Integer, User> usersByLastName();
}
class MyViewModel extends ViewModel {
public final LiveData<PagedList<User>> usersList;
public MyViewModel(UserDao userDao) {
usersList = new LivePagedListBuilder<>(
userDao.usersByLastName(), /* page size */ 20).build();
}
}
class MyActivity extends AppCompatActivity {
private UserAdapter<User> mAdapter;
@Override
public void onCreate(Bundle savedState) {
super.onCreate(savedState);
MyViewModel viewModel = ViewModelProviders.of(this).get(MyViewModel.class);
RecyclerView recyclerView = findViewById(R.id.user_list);
mAdapter = new UserAdapter();
viewModel.usersList.observe(this, pagedList ->
mAdapter.submitList(pagedList));
recyclerView.setAdapter(mAdapter);
}
}
class UserAdapter extends PagedListAdapter<User, UserViewHolder> {
public UserAdapter() {
super(DIFF_CALLBACK);
}
@Override
public void onBindViewHolder(UserViewHolder holder, int position) {
User user = getItem(position);
if (user != null) {
holder.bindTo(user);
} else {
// Null defines a placeholder item - PagedListAdapter will automatically invalidate
// this row when the actual object is loaded from the database
holder.clear();
}
}
public static final DiffUtil.ItemCallback<User> DIFF_CALLBACK =
new DiffUtil.ItemCallback<User>() {
@Override
public boolean areItemsTheSame(@NonNull User oldUser, @NonNull User newUser) {
// User properties may have changed if reloaded from the DB, but ID is fixed
return oldUser.getId() == newUser.getId();
}
@Override
public boolean areContentsTheSame(@NonNull User oldUser, @NonNull User newUser) {
// NOTE: if you use equals, your object must properly override Object#equals()
// Incorrectly returning false here will result in too many animations.
return oldUser.equals(newUser);
}
}
}
Paging Library는 Page 단위 쿼리, Page 나누기, 중복 검사 등의 Paging을 위한 여러 작업을 클래스와 메소드로 정의함으로써, Paging을 더 명확하게 구현하도록 가이드를 제시한다.
6. DataBinding
선언형 형식으로 Data를 UI에 쉽게 Binding하기 쉽게 해주며 findViewById에 의한 객체 획득 번거로움을 제거해주는 라이브러리이다.
즉 UI 구성요소와 앱의 데이터 소스를 선언적으로 연결할 수 있게 하는 라이브러리이다.
레이아웃 파일에서 UI 구성요소를 앱 데이터와 연결하면 액티비티에서 UI 프레임워크의 호출을 줄일 수 있어서 코드가 간결해지고 유지관리가 쉬워진다는 장점이 있다. 또한 앱 성능이 향상되며, 메모리 누수 및 Null Pointer 예외를 방지할 수 있다.
데이터바인딩은 다음 포스팅으로 더 자세히 알아보자.
[Android] DataBinding
Data Binding은 안드로이드 아키텍처 구성요소 중 하나로 UI 구성요소와 앱의 데이터 소스를 선언적으로 연결할 수 있게 하는 라이브러리이다. 레이아웃 파일에서 UI 구성요소를 앱 데이터와 연결하
2bing01.tistory.com
7. Navigation
사용자가 앱 내의 여러 콘텐츠를 Navigation(탐색)하고 그곳에 들어갔다 나올 수 있게 하는 상호작용을 의미한다.
또한 View의 흐름을 직관적으로 보여주기 때문에 앱의 동작흐름을 파악하는데도 도움이 된다.
8. WorkManager
WorkManager는 지속적인 작업에 권장되는 솔루션입니다. 앱이 다시 시작하거나 시스템이 재부팅될 때 작업이 예약된 채로 남아있으면 그 작업은 유지된다. 대부분의 백그라운드 처리는 지속적인 작업을 통해 가장 잘 처리되므로 WorkManager는 백그라운드 처리에 권장하는 기본 API이다.
마무리하며
이번 포스팅을 기록하면서 aac에 대해 자세히 다뤄보며 각각 구성요소가 어떤 용도로 사용되는지 자세히 알아봄으로써 안드로이드 아키텍처 구성요소들에 대해 이해도를 높일 수 있었다. 특히 뷰모델 부분을 공부해보며 mvvm view model에 대해 더 자세히 공부하고 싶은 생각이 들었다.
'Android' 카테고리의 다른 글
[Android Jetpack] 앱 속 DB, Room (0) | 2023.11.16 |
---|---|
[Android] View Binding (1) | 2023.08.30 |
[Android] DataBinding (0) | 2023.08.30 |
[Android] Multi-ViewType RecyclerView (1) | 2023.08.09 |
[Android] RecyclerView (1) | 2023.08.03 |