레이블이 가이드인 게시물을 표시합니다. 모든 게시물 표시
레이블이 가이드인 게시물을 표시합니다. 모든 게시물 표시

2018년 4월 17일 화요일

[안드로이드] Architecture Component 3 - Paging Library 공식문서 번역

Paging Library

페이징 라이브러리는 앱이 데이터 소스로부터 필요한 정보를 점진적으로 읽어오는 작업을 쉽게 만들어주는 라이브러리로, 디바이스에 과부하가 걸리거나, 큰 데이터베이스로부터의 쿼리를 기다리지 않도록 만들어준다.

개요

대부분의 앱들은 많은 양의 데이터를 이용하여 동작하지만, 사실은 화면에 보여야 할 필요가 있는 일부분의 데이터만 불러와서 보여지면 된다. 예를들어, 어떤 앱에서 잠재적으로 보여줄 수 있는 데이터를 몇천개 가지고 있더라도, 한번에 몇십개씩만 엑세스하면 된다. 이부분을 앱이 신경쓰고 있지 않다면, 앱에서 불필요한 데이터까지 모두 요청하게 되고, 이는 불필요한 네트워크 트래픽의 발생과, 디바이스의 성능저하를 초래한다. 데이터가 원격저장소에 저장되거나 동기화되면, 이 또한 앱이 느려지는 원인이 되고, 사용자의 데이터요금을 낭비하게 된다.
기존의 Android API는 콘텐츠의 페이징을 허용했지만 중요한 제약 조건과 단점이 있었다.
  • CursorAdapter는 데이터베이스의 쿼리결과를 ListView의 항목들에 쉽게 매핑시켜주었지만, 데이터베이스 쿼리를 UI 스레드에서 하는것과 Cursor를 이용하기 때문에 데이터의 페이징을 비효율적으로 하는 단점이었다. 더 많은 정보를 확인하려면 Large Database Queries on Android 블로그 글을 확인한다.
  • AsyncListUtil은 RecyclerView에서 포지션 기반의 페이징을 허용하지만, 포지션 기반이 아닌 페이징은 허용하지 않는다. 또한, 셀수 있는 데이터셋에서 null을 플레이스 홀더로 강제로 사용한다.
새로운 페이징 라이브러리는 이러한 문제들을 해결한다. 이 라이브러리는 필요한 데이터를 요청하는 프로세스를 간소화하는 몇몇 클래스들을 포함한다. 이 클래스들은 Room과 같은 기존의 아키텍쳐 컴포넌트 라이브러리들과도 원활하게 잘 동작하도록 구성되어 있다.

기능

페이징 라이브러리의 클래스들을 사용하면, 이 섹션에서 소개되는 작업들을 수행하도록 도와준다.

데이터를 어떻게 가져오는지 정의하기

DataSource클래스를 이용하여 페이징된 데이터를 얻어올 데이터 소스를 정의한다. 데이터 접근 방법에 따라, 이 클래스를 상속받아 서브클래스로 구성할 수도 있다.
  • 로드할 데이터가 다음 / 이전 키를 포함하고 있다면, PageKeyedDataSource 클래스를 사용한다. 예를들어, 네트워크로부터 소셜미디어의 게시물을 가져올 경우, 연속적인 다음페이지를 로드하기 위해서, 다음페이지의 토큰을 전달해야 할 수도 있다.
  • 항목 N의 데이터를 이용해 N+1의 데이터를 가져와야 한다면, ItemKeyedDataSource클래스를 사용한다. 예를들어, 스레드된 코멘트들을 가져와야 한다면, 다음 코멘트를 가져오기 위해, 하나의 코멘트의 아이디를 전달해야 할 수도 있다.
  • 데이터 저장소에서 선택한 위치로부터 데이터의 페이지를 가져와야한다면, PositionalDataSource클래스를 사용한다. 이 클래스는 "1200번 위치로부터 20개의 데이터 반환"과 같이, 선택된 위치로부터 시작하는 데이터의 집합을 요청할 수 있다.
만약 Room 라이브러리를 사용하여 데이터를 관리하고 있다면, 이 Room 라이브러리는 자동적으로 PositionalDataSource인스턴스를 생성하기 위해 DataSource.Factory를 발생시킬 수 있다.
@Query("select * from users WHERE age > :age order by name DESC, id ASC")
DataSource.Factory<Integer, User> usersOlderThan(int age);

메모리로부터 데이터 로드

PagedList클래스는 DataSource로부터 데이터를 불러옵니다. 개발자는 데이터가 한번에 얼마나 로드가 되어야 하고, 미리 패치되어야 하는지 설정하여, 사용자가 데이터로드가 완료될 때까지 기다려야 하는 시간을 최소화할 수 있다. 이 클래스는 RecyclerView.Adapter와 같은 다른 클래스에게 업데이트 신호를 제공하여, 페이지가 로드되었을 때 RecyclerView의 컨텐츠를 업데이트하도록 도와준다.
앱의 구조에 따라, PagedList 클래스 사용을 위한 몇몇 옵션들이 있다. 더 많은 내용을 보려면, 다음의 데이터 로딩 구조를 참고한다.

UI에 데이터 나타내기

PagedListAdapter 클래스는 PagedList로부터 데이터를 UI에 나타내기 위한 RecyclerView.Adapter의 구현체이다. 예를들어, 새로운 페이지가 로드되었을 때, PagedListAdapter는 데이터가 갱신되었음을 RecyclerView에게 알려준다. 이는 RecyclerView가 실제 항목으로 플레이스홀더를 대체하도록 하여 적절한 애니메이션을 수행할 수 있다.
또한 PagedListAdapter는 백그라운드 스레드를 사용하여 한 PagedList에서 다른 PagedList 로의 변경 사항을 계산하고 (예 : 데이터베이스 변경이 업데이트 된 데이터로 새 PagedList를 생성하는 경우) 필요에 따라 목록 내용을 업데이트하기 위해 notifyItem ... () 메소드를 호출한다. 그리고나서 RecyclerView는 필요한 변화를 수행한다. 예를들어, PagedList 버전사이에 항목들의 위치가 변경되면, RecyclerView는 해당 항목을 새로운 위치로 애니메이션으로 움직인다.

데이터 갱신 관찰하기

페이징 라이브러리는 실시간 업데이트가 가능한 PagedList 컨테이너를 생성하기 위한 클래스들을 제공하고 있다.
  • LivePageListBuilder : 이 클래스는 개발자가 제공한 DataSource.Factory를 통해 LiveData<PagedList>를 생성한다. 다음의 샘플코드에서 보는것처럼, 만약 데이터베이스를 관리하기 위해 Room 라이브러리를 사용한다면, DAO는 PositionalDataSource를 사용하여 DataSource.Factory를 생성할 수 있다.
LiveData<PagedList<Item>> pagedItems =
        LivePagedListBuilder(myDataSource, /* page size */ 50)
                .setFetchExecutor(myNetworkExecutor)
                .build();
  • RxpagedListBuilder : 이 클래스는 LivepagedListBuilder와 유사하게, RxJava2 기반의 기능을 제공한다. 이 클래스는 아키텍쳐 라이브러리의 RxJava2(android.arch.paging:rxjava2:1.0.0-alpha1)기반으로 제공된다. 다음의 코드에서 볼 수 있듯이, 이 클래스를 사용하여, PagedList를 구현할 때 Flowable과 Observable을 생성할 수 있다.
Flowable<PagedList<Item>> pagedItems =
        RxPagedListBuilder(myDataSource, /* page size */ 50)
                .setFetchScheduler(myNetworkScheduler)
                .buildFlowable(BackpressureStrategy.LATEST);

데이터 흐름 생성하기

페이징 라이브러리의 컴포넌트는 백그라운드 스레드의 생산자로부터 UI스레드의 화면 출력까지의 데이터 흐름을 구성한다. 예를들어, 새로운 항목이 데이터베이스에 추가되면, DataSource는 갱신될 것이고, LiveData<PagedList> / Flowable<PagedList> 는 백그라운드 스레드에서 새로운 PagedList를 생산한다.
paging-threading.gif
Figure1 : 페이징 라이브러 컴포넌트들은 대부분의 작업을 백그라운드 스레드에서 수행한다. 그래서 UI스레드에 큰 부담이 되지 않는다.
새로 생성된 PagedList는 UI스레드에서 PagedListAdapter로 보내진다. PagedListAdapter는 백그라운드 스레드에서 DiffUtil을 이용하여, 기존의 리스트와 새로운 리스트 사이의 차이를 계산한다. 비교가 끝났으면, PagedListAdapter는 리스트의 차이점을 이용하여 RecyclerView.Adapter.notifyItemInserted() 를 호출하여 UI에 항목들을 삽입한다.
UI스레드의 RecyclerView는 삽입된 아이템을 확인하여 적절하게 애니메이셔닝 한다.

데이터베이스 예제

다음의 코드조각들은 모든 라이브러리들이 함께 동작하도록 하는 몇몇 방법을 보여준다.

LiveData를 이용하여 페이징된 데이터 관찰하기

다음의 코드는 모든 컴포넌트들이 함께 동작하는 것을 보여준다. 사용자가 데이터베이스에 추가, 삭제, 변경할 때, RecyclerView의 내용은 자동적으로, 효율적으로 갱신된다.
@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);
        }
    }
}

RxJava2를 이용하여 페이징된 데이터 관찰하기

LiveData대신 RxJava2를 사용하려고 한다면, Observable이나 Flowable 객체를 생성한다.
class MyViewModel extends ViewModel {

    public final Flowable<PagedList<User>> usersList;

    public MyViewModel(UserDao userDao) {
        usersList = new RxPagedListBuilder<>(userDao.usersByLastName(),
                /* page size */ 50).buildFlowable(BackpressureStrategy.LATEST);
    }
}
그리고, 다음의 코드와 같이 해당 데이터의 관찰을 시작 / 중지할 수 있다.
class MyActivity extends AppCompatActivity {
    private UserAdapter<User> mAdapter;
    private final CompositeDisposable mDisposable = new CompositeDisposable();

    @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();
        recyclerView.setAdapter(mAdapter);
    }

    @Override
    protected void onStart() {
        super.onStart();
        myDisposable.add(mViewModel.usersList.subscribe(flowableList ->
                mAdapter.submitList(flowableList)));
    }

    @Override
    protected void onStop() {
        super.onStop();
        mDisposable.clear();
    }
}
UserDao, UserAdapter 클래스의 코드는 LiveData의 예제와 같으므로 생략한다.

데이터 로딩 구조 선택하기

페이징 라이브러리를 이용해 데이터를 페이징하는 두가지의 기본적인 방법이 있다.

네트워크 or 데이터베이스

첫번째로, 하나의 소스(로컬저장소이든 네트워크이든)로부터 페이징을 할 수 있다. 이 경우 다음 샘플과 같이 LiveData를 이용하여 로드된 데이터를 UI에 전달한다.
데이터의 소스를 지정하려면 LivePagedListBuilder에 DataSource.Factory를 전달한다.
paging-network-or-database.png
Figure2 : 하나의 데이터소스는 DataSource.Factory를 제공하여 컨텐츠를 로드한다.
데이터베이스를 관찰할 때, 컨텐츠의 변화가 발생하면, 데이터베이스는 새로운 PagedList에 푸쉬한다. 네트워크 페이징인 경우에(백엔드가 업데이트를 보내지 않을 때), 스와이프 리프레시 같은 시그널은 현재 데이터소스를 무효화함으로써 새로운 PagedList를 받아올 수 있다. 이는 모든 데이터를 비동기적으로 새로고침한다.
PagingWithNetworkSample의 메모리 + 네트워크 저장소 구현은 스와이프 리프레시, 네트워크 에러, 재시도를 처리하는 동안, Retrofit을 이용하여 어떻게 네트워크 DataSource.Factory를 구현했는지를 보여준다.

네트워크 and 데이터베이스

두번째로, 로컬저장소로부터 데이터를 페이지할 수 있고, 로컬저장소는 네트워크로부터 추가 데이터를 페이징한다. 이는 종종 네트워크 연결을 최소화하고, 백엔드의 캐시로 데이터베이스를 사용하여 더 나은 연결지양 경험을 제공한다.
이 경우, LiveData를 이용하여 데이터베이스로부터 컨텐츠를 페이징하고, 최신 데이터의 시그널을 관찰하기 위해, LivePagedListBuilder에 BoundaryCallback을 전달한다.
paging-network-plus-database.png
Figure3 : 데이터베이스는 네트워크 데이터의 캐시로 이용된다. UI는 데이터베이스로부터 데이터를 로드하고, 데이터가 없을 때, 네트워크로 신호를 보내서 네트워크로부터 데이터베이스로 데이터를 로드한다.
이후, 이 콜백을 네트워크 요청에 연결하면 데이터가 데이터베이스에 바로 저장된다. UI는 데이터베이스의 갱신을 구독하고 있으므로, 새로운 데이터의 변경은 자동적으로 UI로 전달된다.
PagingWithNetworkSample의 데이터베이스 + 네트워크 저장소 구현은 스와이프 리프레시, 네트워크 에러, 재시도를 처리하는 동안, Retrofit을 이용하여 어떻게 네트워크의 BoundaryCallback을 구현하는지 보여준다.

원문 : https://developer.android.com/topic/libraries/architecture/paging.html


2016년 10월 5일 수요일

[안드로이드] 성능향상 팁

1.불필요한 객체생성을 피하라

불필요한 객체생성은 메모리의 낭비를 가져오게 되고, 결국에는 GC작업으로 이어지게 된다. GC는 매우 비싼 연산이므로, 성능에 매우 악영향을 미치게 된다. (2.3에서 동시 가비지 컬렉터가 소개되긴 했지만, 그럼에도 비싼 연산임을 유념하자!)
다음은 가장 흔한 몇 가지의 예를 보여준다.
  • 어떤 함수가 String을 반환하고, 그 반환값이 항상 StringBuffer에 추가되는 경우가 있다면, 메서드를 변경하여 내부에서 직접 StringBuffer에 추가하도록 한다.
  • 어떤 입력된 String 데이터로부터 문자열을 추출하는 경우, 새로 문자열을 생성하는 대신 기존 데이터의 substring() 메서드를 이용하도록 한다.
  • Integer 등 Wrapper 배열을 이용중이라면, primitive 타입 배열로 변경을 고려한다.
  • (Object1, Object2) 튜플 형태의 배열 1개를 이용중이라면, Object1[], Object2[]의 배열 2개로 변경을 고려한다.(단, API 제작 등 성능보다는 디자인패턴이 중요할 경우 예외)
즉, GC 작업을 피하기 위해, 불필요한 객체생성은 가능하면 피하는 것이 좋다.

2.Virtual 메서드보다는 Static 메서드를 이용하라

만약, 메서드 내에서 객체의 멤버필드를 이용하지 않는다면, 메서드를 static으로 변경하는 것을 고려한다. 메서드 실행이 대략 15~20% 정도 빨라질 것이다. 또한, 메서드 호출이, 객체의 상태를 바꾸지 못하는 것을 메서드 원형을 통해 바로 알 수 있어서 코드 분석하기 좋다.

3.상수에는 static final 키워드를 이용하라

static int intVal = 42;
static String strVal = "Hello";
상수를 선언하기 위해 다음과 같이 final 키워드 없이 static 필드를 이용하게 되면, 컴파일러가 이 값들을 해당 변수에 할당하고, 이 변수들을 검색테이블에 저장한다. 나중에 이 변수들을 이용하려면, 컴파일러는 이 검색테이블을 통해 변수를 검색하여 값을 얻어오게 된다.
static final int intVal = 42;
static final String strVal = "hello";
다음과 같이 final 필드를 함께 이용할 경우, 이 값들은 dex 파일의 static 영역에 함꼐 저장된다. 이 경우, 변수를 이용하려면 컴파일러는 검색테이블을 거치지 않고 바로 값에 접근한다. 그렇기에 일반적으로는 static final 필드가 변수 접근속도가 빠르다.
단, 이는 primitive 타입과 String 상수에 한해 유효하다.

4.클래스 내부에서의 Getter/Setter는 피하라

일반적인 OOP 네이티브 언어(C++, C#, JAVA 등)는 멤버필드의 Getter/Setter를 지향하며, 클래스 내부에서도 사용하기를 권장한다. 이는 컴파일러가 대부분 인라인으로 엑세스하기 때문에 성능이슈가 거의 없고, 필요에 따라 변수에 제약조건을 둘 수 있기 때문이다.
하지만, 안드로이드에서는 같은 자바 언어를 사용하지만 컴파일러의 동작이 다르기 때문에 클래스 내부에서의 Getter/Setter 사용은 변수에 바로 접근하는 것보다 매우 비싼 연산이다.(심지어 변수를 검색테이블에서 찾는 것 보다도 느리다.) 그러므로, 자신의 멤버필드를 접근할 때는 가급적 Getter/Setter를 피하고 직접 이용하도록 한다.
Note : 클래스 외부에서 Getter/Setter를 이용하지 말자는 의미가 아니다. OOP를 지향하기 위해 클래스 외부에서는 Getter/Setter를 이용하는 것이 좋다.

5.향상된 for 문을 이용하라

자바 1.5부터 지원하고 있는 향상된 for문 (일명 for-each문)은 일반 배열과 Collections 객체에 대해 이용할 수 있다. Collections를 향상된 for문으로 돌리면 컴파일러가 Iterator를 이용하는 방법으로 바꿔서 컴파일한다.(즉, 명시적으로 Iterator를 이용하는 것과 동일하다.) 일반 배열의 경우에는 성능차이가 많이 날 수 있다.
static class Foo {
int mSplat;
}

Foo[] mArray = ...

public void zero() {
int sum = 0;
for (int i = 0; i < mArray.length; ++i) {
sum += mArray[i].mSplat;
}
}

public void one() {
int sum = 0;
Foo[] localArray = mArray;
int len = localArray.length;

for (int i = 0; i < len; ++i) {
sum += localArray[i].mSplat;
}
}

public void two() {
int sum = 0;
for (Foo a : mArray) {
sum += a.mSplat;
}
}
  • zero() 메서드는 매 루프마다 mArray.length를 계속 접근하기 때문에 가장 느리다.
  • one() 메서드는 멤버변수의 검색을 피하기 위해서 로컬변수에 대입해 이용하고 있고, length도 매번 불리지 않도록 메모리에 캐싱해두었기 때문에 zero() 메서드보다 빠르다.
  • two() 메서드는 JIT가 없는 디바이스에서는 앞의 두 메서드보다 빠르고, JIT가 있는 디바이스에서는 one() 메서드와 거의 같은 성능을 보인다.

6.Private Inner 클래스에서의 Private 멤버/메서드 접근은 가급적 피하라

public class Foo {
private class Inner {
void stuff() {
Foo.this.doStuff(Foo.this.mValue);
}
}

private int mValue;

public void run() {
Inner in = new Inner();
mValue = 27;
in.stuff();
}

private void doStuff(int value) {
System.out.println("Value is " + value);
}
}
다음 예시에서 private inner 클래스인 Inner는 자신의 outer 클래스인 Foo의 private 필드와 메서드를 이용하고 있다. 이는 자바의 문법으로는 가능하며, 컴파일도 잘 되고, 실행값도 우리가 예상하는 대로 "Value is 27"이 출력될 것이다.
그러나, VM은 Foo와 Foo$Inner를 전혀 별개의 클래스로 판단하고, 자바문법이 허용했지만 Inner에서의 private 필드/메서드의 접근이 잘못되었다고 인식한다. 따라서, VM은
이를 해결하기 위해 다음과 같은 브릿지 메서드를 내부적으로 생성한다.
/*package*/ static int Foo.access$100(Foo foo) {
return foo.mValue;
}
/*package*/ static void Foo.access$200(Foo foo, int value) {
foo.doStuff(value);
}
이 브릿지 메서드는 Inner에서 mValue를 접근하거나, doStuff() 메서드를 호출할 때마다 호출된다. 이는 당연히 필드를 직접 엑세스하는 것보다 느리다. 브릿지 메서드의 생성을 피하려면, Foo의 멤버/메서드를 private이 아닌 package 접근제어자로 변경해주면 된다. 하지만, 변경된 멤버/메서드는 같은 패키지 내의 다른 클래스에서 이용될 수 있기 때문에 OOP에 위배되므로 잘 고려해서 변경해야 한다.

7.부동소수점 사용을 피하라

안드로이드 디바이스에서는 부동소수점 연산은 정수 연산보다 약 2배정도 느리다. 속도 관점에서 보면, double과 float은 별 차이가 없다. 그러므로, 어차피 부동소수점 연산을 해야하고 메모리의 제약이 덜하다면, 2배 더 정밀한 표현이 가능한 double을 이용하는 것이 낫다.

8.시스템 Library를 잘 알고, 사용하자

먼저 코드를 구현하기 전에, 시스템에서 지원하는 라이브러리 중에 구현하고자 하는 동작을 지원하는지 확인하여 가급적 시스템이 제공하는 라이브러리를 이용하도록 한다. 이 라이브러리에서 제공하는 메서드들은 JIT 등, 컴파일러에 최적화되어 있어서, 대부분의 경우 유저가 직접 구현한 방법보다 빠르거나 같다.

9.주의해서 Native 메서드를 이용하라

대부분의 경우 NDK를 사용할 필요가 없을 정도로 JAVA에서 최적화를 진행할 수 있다. 그러나, 메모리, 파일 디스크립터 등의 네이티브 자원을 할당해야 하거나, 아키텍쳐별로 컴파일을 따로 해줘야 할 필요가 있는 등의 이유가 있다면, NDK를 이용한 네이티브 코드를 이용하는 것도 고려해 볼 수 있다.

성능에 대한 오해

JIT가 없는 디바이스에서는, 인터페이스 타입의 변수를 선언하고 그 인터페이스의 메서드를 이용하는 것이 실제 타입의 변수를 선언하고 메서드를 이용하는 것보다 조금 느린 것이 사실이다.(약 6%) 그러나 JIT가 있는 디바이스에서는 성능차이가 없다. 그러므로 6%의 얼마안되는 성능차이를 커버하고자 OOP의 안티패턴인 실제타입 변수를 선언하는 것은 재고해볼 필요가 있다.

참고