레이블이 구성인 게시물을 표시합니다. 모든 게시물 표시
레이블이 구성인 게시물을 표시합니다. 모든 게시물 표시

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();
    }
}
UserDaoUserAdapter 클래스의 코드는 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년 11월 7일 월요일

[자바] Effective Java 4장 - 클래스와 인터페이스1

클래스와 인터페이스1

1. 클래스와 멤버의 접근권한은 최소화하라

  • 정보은닉 : 필요한 정보 외의 모든 구현 세부사항을 다른 모듈에서 접근하지 못하도록 감추는 것
  • 정보은닉은 왜 중요한가?
    • 모듈간 의존성이 낮아진다. -> 재사용, 리팩토링이 쉽다.
    • 효율적인 성능튜닝을 할 수 있다. -> 모듈별로 모니터링하기 쉽다.
  • 클래스에서의 정보은닉 : 불필요한 멤버필드 & 메서드를 접근권한을 통해 감추는 것
    • private : 최상위 레벨 클래스 내부에서만 접근 가능.
    • package-private(default) : 같은 패키지 내의 아무 클래스나 접근 가능. 자바에서는 명시적으로 접근권한을 설정하지 않으면 기본적으로 이 권한이 주어진다.
    • protected : 해당 클래스 및 그 하위 클래스에서만 접근 가능
    • public : 어디에서든 접근 가능
  • 멤버필드는 절대로 public 권한으로 선언하면 안된다.
  • 꼭 공개가 필요한 멤버필드는 public static final 을 이용해 공개하려는 변수를 상수로 선언하는 것을 고려해본다. (단, 기본타입에 한해서... 참조타입은 상수이지만 값을 변경할 수 있다.)

2. 클래스 안에는 public 필드를 두지 말고 접근자 메서드를 사용한다.

  • pulbic 멤버필드는 정보은닉의 이점을 누릴 수 없다.
  • 멤버필드를 private / protected로 변경하고, public gettersetter 접근 메서드를 이용하면 정보은닉의 이점을 100% 누릴 수 있다.
  • package-private / private 중첩 클래스의 멤버필드는 경우에 따라서 public으로 선언하여 코드가독성을 높이기도 한다.
    • 이 중첩클래스들은 바깥의 클래스만 접근이 가능하기 때문에, 정보은닉을 어기는 사례가 아님.

3. 변경 가능성을 최소화한다.

  • 변경 불가능(immutable) 클래스 : 객체를 수정할 수 없는 클래스(ex String 클래스)
  • 변경 불가능 클래스의 이점
    • 설계&구현이 쉽다.
    • 오류 가능성이 적다.
    • 멀티 쓰레드에서 동시성 접근에 대한 걱정을 안해도 된다.
  • 변경 불가능 클래스의 단점
    • 값마다 객체를 새로 생성해야 한다.
  • 변경 불가능 클래스 만드는 방법
1. 객체 상태를 변경하는 setter 메서드를 제공하지 않는다.
2. 상속 받을 수 없도록 한다. class를 선언할 때 final 키워드를 이용하면 상속금지 클래스가 된다.
3. 모든 필드를 final로 선언한다.
4. 모든 필드를 private으로 선언한다.
5. 변경 가능 필드를 제공해야 한다면, 복사본을 만들엉 제공한다.

4. 상속하는 대신 구성한다.

  • 상속 : 클래스를 재사용하기 위해 해당 클래스의 자식 클래스로 구현하는 방법. 부모 클래스로 부터 private 이외의 필드, 메서드를 물려받을 수 있다.
  • 상속의 단점
    • 부모 클래스가 변경되면, 그 여파가 자식클래스까지 미친다.
class A {
    protected int age;
    public int getAge() {
        return age;
    }
}

class B extends A {
    @Override
    public int getAge() {
        age += 5;
        return getAge();
    }
}
  • 구성 : 물려받고자 하는 클래스의 기능을 이용하기 위해 새로운 클래스 내부에 해당 클래스를 private 필드로 선언하고, 이용하고자 하는 메서드를 호출하여 결과를 반환하도록 래핑(Wrapping)하는 방법
  • 구성의 단점
    • callback 구현방식과 함께 사용하기에는 적합하지 않다.
class A {
    private int age;
    public int getAge() {
        return age;
    }
}

class B {
    private A a;
    public getAge() {
        return a.getAge() + 5;
    }
}
  • 즉, 상속이 꼭 맞는 경우가 아니라면 구성 방법을 고려한다.

5. 상속을 위한 설계와 문서를 갖추거나, 그럴 수 없으면 상속을 금지한다.

  • 일반적으로 상속 가능 클래스를 설계하는 것 보다, 구성 방법을 고려하거나, 가능하다면 변경 불가능 클래스로 만들어 상속을 금지하는 것이 훨씬 쉽다.
  • 상속 클래스를 작성하려면 적절한 개수의 protected 멤버를 유지한다.
    • protected 멤버가 너무 많으면 관리하기 힘들다.
    • protected 멤버가 너무 적으면 상속하는 의미가 별로 없는 클래스가 된다.
  • 상속을 허용하는 클래스를 작성할 때의 주의사항
1. 생성자에서 재정의 가능 메서드를 호출하면 안된다. 상속관계에서 부모클래스의 생성자가 자식클래스의 생성자보다 먼저 불리기 때문에, 재정의 가능 메서드의 값이 하위 클래스 생성자에 의존하게 되면 문제가 생긴다.
2. Cloneable, Serializable 인터페이스를 구현하는 클래스라면, clone(), readObject() 메서드도 생성자와 같은 역할을 하므로, 위의 규칙을 지켜야 한다.
3.  상속 가능한(public / protected) 메서드가 내부적으로 어떻게 동작하는지 문서화해야 한다. 자식클래스가 그 메서드를 재정의할 때, 그 문서를 보고 부모 메서드의 행동을 가늠하여 구현되기 때문이다.