[Android] AAC(Android Architecture Components) 정리

AAC

  • 테스트와 유지보수가 쉬운 앱을 디자인할 수 있도록 돕는 라이브러리의 모음이다.
  • 안드로이드 앱을 만들면서 만날 수 있는 문제들을 쉽게 해결할 수 있도록 한다.
  • 총 5개의 라이브러리(Lifecycles, LiveData, ViewModel, Room, Paging)로 구성되어 있다.
  • 안드로이드는 Activity, Service, BroadcastReceiver, ContentProvider 컴포넌트들이 있고, 생명주기가 다르기 때문에 이러한 컴포넌트들을 잘 고려&연결하여 앱을 잘 만들어야 하는데, 구글이 이 고민을 줄이기 위해 AAC를 만들었다.

1. Lifecycles

  • 생명주기의 모니터링을 돕는다.
  • 크게 두 가지로 구성되어 있다.

(1) Lifecycle Owner

  • 자신의 생명주기를 담은 객체이다.
  • Activity, Fragment에서 생명주기르 분리하여 Lifecycle 객체에 담는다.
  • Lifecycle 객체를 통해 다른 곳에서 해당 화면의 생명주기를 모니터링할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class MainActivity extends LifecycleActivity {
    private MyLocationListener myLocationListener;
    public void onCreate(Bundle savedInstanceState) {

        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;

        if (lifecycle.getCurrentState().isAtLeast(STARTED)) {
            // connect if not connected
        }
    }
}

(2) Lifecycle Observer

  • 화면밖에서도 생명주기에 따른 동작을 정의하기 위해서 원하느 클래스에 LifecycleObserver 인터페이스를 구현하고, 넘겨받은 Lifecycle Owner객체에 구현한 LifecyclerObserver를 등록해야 한다.
  • annotaion을 이용하여 Lifecycle Owner의 생명주기에 따른 동작할 메소드를 정의할 수 있다.
  • Lifecycles를 통해 화면 밖에서 화면의 생명주기를 모니터링하고, 동작을 정의할 수 있다. -> 더 직관적인 생명주기 프로그래밍을 가능하게 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MyLocationListener implements LifecycleObserver {
    public MyLocationListener(Context context, Lifecycle lifecycle, Callback callback) {
        lifecycle.addObserver(this);
    }
    
    @OnLifecycleEvent(Lifecycle.Event.ON_START)
    void start() {
        // Do something
    }
    
    @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
    void stop() {
        // Do something
    }
}

2. LiveData

  • 데이터를 개선된 Observable로 wrapping하여 생명주기와 데이터 변경을 인지할 수 있도록 한다.
  • 데이터 모델을 wrapping해서 생명주기와 데이터 변경을 자연스럽게 모델 스스로 인지할 수 있도록 한다.
  • 위에서 예시로 든 MyLocationListener를 LiveData로 다시 구현할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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()가 불린다.
1
2
3
4
5
6
7
8
9
class MainActivity extends LifecycleActivity {
    public void onCreate(Bundle savedInstanceState) {
        LiveData<Location> myLocationListener = new MyLocationListener();
        // Active, Inactive를 판단하기 위해 observe() 할 때 Lifecycle를 넘긴다.
        myLocationListener.observe(this, location -> {
            // Data가 변경되면 동작할 콜백을 등록
        });
    }
}
  • Observer는 LiveData.observe()로 등록하며, 이 메소드로 데이터 변경을 구독한다.
  • Active observer는 생명주기가 최소 Resumed, Started에 있는 Observer를 말한다.
  • Observer가 생명주기에 따라 active, inactive 상태를 판단하기 위해 observer()를 호출할 때 Lifecycle을 넘긴다.
  • LiveData는 Active observer 개수로 생명주기를 간접적으로 인지한다.
1
2
3
4
5
6
7
8
9
10
class MyLocationListener extends LiveData<Location> {
    private LocationManager locationManager;
    private LocationListener listener = new LocationListener() {
        @Override
        public void onLocationChanged(Location location) {
            // setValue()로 데이터를 변경하고 구독하는 Observer들에게 이벤트를 전달
            setValue(location);
        }
    };
}
  • LiveData는 observe()에서 넘어온 Lifecycle로 생명주기를 모니터링하고, 함께 받은 콜백으로 데이터 변경 이벤트를 구독한다.
  • 콜백은 LiveData.setValue()로 데이터를 변경하면 호출된다.

3. ViewModel

  • 앱의 생명주기를 고려하여 UI 관련 데이터를 저장하고 관리하는 컴포넌트이다.
  • 데이터를 쉽게 생명주기와 분리하여 관리할 수 있도록 돕는다.
  • AAC의 ViewModeel을 상속받은 뷰모델은 ViewModelProviders로 Scpoe를 관리할 수 있다.
  • 해당 Scope내에서는 하나의 인스턴스만을 유지하여 작업이 중복되거나 데이터가 소실되지 않도록 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class MyViewModel extends ViewModel {
    private LiveData<User> userData;
    public LiveData<User> getUser(String userId) {
        if (userData == null) userData = webservice.fetchUser(userId);
        return userData;
    }
}

class MainActivity extends LifecycleActivity {
    public void onCreate(Bundle savedInstanceState) {
        String userId = "userId@gmail.com";
        // 처음이면 this Scope에 종속된 MyViewModel를 생성한다.
        // this Scope에 종속된 MyViewModel가 이미 있다면 불러온다.
        ViewModelProviders.of(this)
                .get(MyViewModel.class)
                // 화면회전이 일어나 다시 호출되어도 같은 인스턴스 이므로 중복작업이 일어나지 않는다.
                .getUser(userId)
                .observe(this, user -> {
                    // update UI
                });
    }
}

4. Room

  • ORM(Object Relation Mapping. Cursor 단위로 통신하는 쿼리를 객체 단위로 통신할 수 있도록 돕는다) 라이브러리 중 하나로, Annotation 기반이다.

(1) Annotation 기반의 정의와 자동 매칭

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// Database 정의. 테이블 및 버전을 함께 적는다.
// RoomDatabase를 상속받는다.
@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
    // Dao를 선언한다.
    public abstract UserDao userDao();
}

// 정의한 Database 객체를 가져온다.
public MyDatabase getMyDatabase() {
    MyDatabase db = Room
            .databaseBuilder(getApplicationContext(), MyDatabase.class)
            .build();
}

// Entity Annotation으로 테이블 정의. 인스턴스 변수들이 곧 Column이다.
@Entity
public class User {
    // PrimaryKey Annotation으로 키를 정의한다.
    @PrimaryKey
    private int uid;
    private String firstName;
    private String lastName;
}

// DAO 정의
@Dao
interface UserDao {
    // Query Annotation으로 쿼리를 정의한다.
    // 파라미터로 전달할 값을 : 기호 다음에 같은 이름으로 선언한다. 여기서는 :first, :last 이다.
    // FROM 절로 넘긴 테이블과 매칭되는 모델로 반환값을 선언하면 알아서 맞는 객체로 매핑해준다. 여기서는 User이다.
    @Query("SELECT * FROM user WHERE first_name :first AND last_name :last")
    User findByName(String first, String last);

    // 파라미터로 객체 그대로를 넘깁니다. 값 매칭은 Room이 인스턴스 변수를 보고 알아서 해줍니다.
    @Insert
    void insertAll(User... users);
    @Delete
    void delete(User user);
    @Update
    void updateAll(List<User> users);
}

(2) 컴파일 타임 쿼리 검증

1
2
3
// User 테이블인데 실수로 FROM에 users로 적었다. 이럴경우 Room은 컴파일 타임에 에러를 뱉어 실수를 빨리 발견하도록 돕는다.
@Query("SELECT uid, firstName, lastName FROM users WHERE uid :uid")
User findByUid(String uid);
  • Room은 원래 런타임으로 테스트해야만 제대로 동작하는지 알 수 있는 쿼리를 컴파일 타임에 검증하여, 정확한 쿼리를 빨리 짤 수 있도록 돕는다.

(3) 실시간 값 추적 (Observability)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// User를 LiveData로 Wrapping하여 반환한다.
@Query("SELECT uid, firstName, lastName FROM user WHERE uid :uid")
LiveData<User> findByUid(String uid);

/*
 * Dao에서 LiveData로 반환된 값을 ViewModel을 거쳐 뷰에서 observe()하면 데이터베이스의 값이 변경될 때 실시간으로 추적할 수 있다.
 */

class MyViewModel extends ViewModel {
    private UserDao userDao;
    private LiveData<User> userData;
    public MyViewModel(UserDao userDao) {
        this.userDao = userDao;
    }
    // Dao에서 LiveData로 반환된 값을 반환한다.
    public LiveData<User> getUser(String userId) {
        if (userData == null) userData = userDao.findByUid("userId");
        return userData;
    }
}

class MainActivity extends LifecycleActivity {
    public void onCreate(Bundle savedInstanceState) {
        String userId = "userId@gmail.com";
        ViewModelProviders.of(this)
                .get(MyViewModel.class)
                .getUser(userId)
                .observe(this, user -> {
                    // Room의 Dao가 반환한 LiveData를 구독함으로써 데이터베이스 변경을 실시간으로 추적한다.
                });
    }
}
  • Room은 LiveData와 연계하여 데이터베이스의 값을 실시간으로 추적할 수 있다.
  • 다른 ORM은 Observability를 구현하려면 많은 고민이 필요하지만, Room은 반환 타입을 LiveData로 바꾸기만 하면 쉽게 구현할 수 있다.

5. Paging

  • 리스트 뷰에서 컨텐츠를 특정 기준으로 범위를 나누고, 스크롤을 따라 범위 단위로 로드되도록 하는 것을 Paging이라고 한다.
  • Paging에는 세 가지 작업이 필요하다.
    1. 데이터를 Page 단위로 가져오는 쿼리
    1. 데이터를 특정 기준으로 Page 나누기
    1. 중복 아이템 검사
  • 위 세 가지를 구현하는 것은 귀찮은 일이 많은데, Paging Library는 이를 쉽게 구현할 수 있도록 도와준다.
  • Paging Library는 세 가지로 구성되어 있다. DataSource, PagedList, PagedListAdapter

(1) DataSource

1
2
3
// 반환 타입을 DataSource로 하면 자동으로 PositionalDataSource를 생성한다.
@Query("select * from users WHERE age > :age order by name DESC, id ASC")
DataSource.Factory<Integer, User> usersOlderThan(int age);
  • Local 또는 Network에서 데이터를 가져오는 쿼리를 담고 있다.
  • PagedKeyedDataSource, ItemKeyedDataSource, PositionalDataSource 3가지가 있으며, 키 속성에 따라 맞는 클래스를 상속받아 쿼리를 구현해야 한다.

(2) PagedList

1
2
3
4
LiveData<PagedList<Item>> pagedItems =
        LivePagedListBuilder(myDataSource, /* page size */ 50)
                .setFetchExecutor(myNetworkExecutor)
                .build();
  • Page 속성과 DataSource를 가지고 있다. Page 속성에 맞춰 DataSource로 맞는 범위를 데이터로 불러온다.
  • LivePagedListBuilder에 Page 속성과 DataSource를 정의하고 빌드하면, LiveData<PagedList> 타입을 반환한다.

(3) PagedListAdapter

1
2
3
4
5
6
7
8
9
10
11
12
// 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);
        }
    }
  • PagedList를 UI에 보여주고 아이템 중복을 검사한다. 다음 Page에 도달하면 데이터를 PagedList에게 요청한다.
  • Paging은 Page가 밀리는 등 여러 경우에 아이템이 밀려 중복이 발생할 수 있따. DiffUtil에 기준을 정의해 PagedListAdapter의 Constructor에 넘겨주면, 기준에 걸리는 중복 아이템은 UI에 보여주지 않는다.

MVP에 AAC를 활용하는 방법

1. MVP with full AAC

  • 구글이 가이드하는 MVVM에 Presenter를 추가한 모습이다.
  • ViewModel은 Model에 집중하고, Presenter가 View와 통신한다. -> 역할 세분화
  • 하지만 기본 MVP에 MVVM을 넣어야 하므로 레거시를 활용할 수 없고 새롭게 설계해야 한다.

2. MVP with part of AAC

  • 1에서 ViewModel을 뺀 모습이다.
  • 기존 MVP를 유지하면서 필요한 부분에만 AAC를 활용하는 방식이다.
  • MVP 레거시에서 가장 쉽게 AAC를 적용할 수 있는 설계이다.

3. MVP with LiveData

  • 2에서 ViewModel의 LiveData를 Presenter에서 사용한다.
  • Presenter에서 통신하는 데이터를 실시간으로 모니터링할 수 있다.
  • MVP를 유지하면서도 대부분의 AAC를 활용한다.

4. MVP without AAC

  • 대부분의 실무앱은 RxJava로 데이터를 통신하므로 3번처럼 적용하기 쉽지 않다.
  • 그래서 LiveData의 실시간 모니터링을 RxJava를 활용하여 직접 구현하였다.
  • Repository에 Observer를 두어 데이터가 변경될 때마다 이벤트를 보내고, 구독하는 UI에서 모니터링한다.
참고