2018년 2월 19일 월요일

[안드로이드] Architecture Component 1 - ViewModel 공식문서 번역

ViewModel

ViewModel 클래스는 라이프사이클을 고려하여, UI 관련된 데이터를 저장하고 관리하기 위해 설계되었다. ViewModel 클래스는 화면회전과 같이 설정이 변경되는 상황에서도 data가 계속 남아있을 수 있도록 해준다.
  • Note : ViewModel을 프로젝트에 추가하려면 다음 링크를 확인한다.
안드로이드 프레임워크는 ActivityFragment와 같은 UI 컨트롤러의 라이프사이클을 관리한다. 프레임워크눈 사용자의 특정한 동작이나 완전히 예상치못한 장치의 이벤트에 대한 응답으로 UI 컨트롤러를 파괴(Destroy)하고 재생성(re-create)하는 것을 결정하기도 한다.
시스템이 UI 컨트롤러를 파괴 & 재생성하면, 그 안에 저장해두었던 UI 관련 임시데이터들은 모두 사라진다. 예를들면, 앱에는 사용자의 목록이 포함되어 있을 수 있다. 설정이 변경되어 Activity가 재생성될 때, 새로운 Activity 인스턴스는 사용자목록을 다시 불러와야 한다. 단순한 데이터일때는 Activity의 onSaveInstanceState()메서드를 이용하여 Bundle에 저장하고 onCreate()에서 Bundle에 저장된 데이터를 불러올 수 있지만, 이는 데이터의 양이 적고, 해당 데이터가 직렬화/역직렬화가 가능할 때에만 해당하는 방법이다. 잠재적으로 데이터의 양이 많다면 적절한 방법이 아니다.
또 다른 문제점으로는, UI 컨트롤러가 자주 비동기호출을 만들어서, 반환하는데 다소 시간이 걸릴 수 있다. 잠재적인 메모리 누수를 피하기 위해, UI컨트롤러는 Destroy 된 후에 시스템이 instance를 정리하기 전에 비동기 호출을 관리할 필요가 있다. 이러한 관리작업은 엄청난 유지보수를 필요로 하고, 설정변화에 의해 인스턴스가 재생성되는 경우에 이전에 구성했던 데이터들을 버리고 새로 구성해야하기 때문에 리소스가 낭비된다.
Activity, Fragment와 같은 UI 컨트롤러는 주로 사용자에게 UI 데이터를 보여주고, 사용자의 액션에 반응하고, 권한요청과 같이 OS의 요청을 처리하는 용도로 사용된다. 데이터베이스나 네트워크로부터 데이터를 불러오는 동작을 UI 컨트롤러에서 수행하면 클래스의 부피가 커지게 된다. UI 컨트롤러에 과도하게 책임이 걸려있으면 자칫 클래스가 단일화되어 테스트가 매우 어려워 질 수 있다.
ViewModel은 UI 컨트롤러의 로직으로부터 UI 데이터 로직을 분리할 수 있는 매우 효과적인 방법이다.

ViewModel 구현

구글에서 제공하는 Architecture Components에서는, UI 데이터를 직접 준비하는 UI 컨트롤러들을 위해서 ViewModel과 연동할 수 있도록 헬퍼클래스를 제공한다. ViewModel 객체는 앱의 설정변경이 일어나도 데이터를 유지하므로,설정변경 후의 새로운 Activity, Fragment 인스턴스에서도 바로 데이터를 사용할 수 있다. 예를 들어, Activity나 Fragment에서 사용자 목록 화면을 구성해야 한다면, 다음의 예제코드처럼 ViewModel 내부에서 해당 데이터를 불러오고, 유지 관리하도록 구현해야 한다.
public class MyViewModel extends ViewModel {
    private MutableLiveData<List<User>> users;
    public LiveData<List<User>> getUsers() {
        if (users == null) {
            users = new MutableLiveData<List<Users>>();
            loadUsers();
        }
        return users;
    }

    private void loadUsers() {
        // Do an asyncronous operation to fetch users.
    }
}
이후에, 다음과 같이 Activity에서 그 리스트를 접근하도록 할 수 있다.
public class MyActivity extends AppCompatActivity {
    public void onCreate(Bundle savedInstanceState) {
        // Create a ViewModel the first time the system calls an activity's onCreate() method.
        // Re-created activities receive the same MyViewModel instance created by the first activity.

        MyViewModel model = ViewModelProviders.of(this).get(MyViewModel.class);
        model.getUsers().observe(this, users -> {
            // update UI
        });
    }
}
Activity가 재생성되어도 처음의 Activity 인스턴스에서 생성된 MyViewModel 인스턴스와 같은 인스턴스를 접근할 것이다. ViewModel을 소유한 Activity가 종료되면, 프레임워크는 ViewModel 객체의 onCleared() 메서드를 호출할 것이다. 개발자는 그 메서드를 오버라이드하여 리소스를 해제하도록 구현해야 한다.
  • Caution : ViewModel 인스턴스는 반드시 뷰, Lifecycle, Activity 참조를 가지고있는 어떤 클래스도 참조를 유지하면 안된다.
ViewModel 인스턴스는 뷰나 LifecyclerOwners의 특정 인스턴스보다 오래 유지되도록 설계되었다. 이러한 설계는 ViewModel 인스턴스가 뷰나 Lifecycler 인스턴스를 모르기 때문에, ViewModel에 대해 더 쉽게 테스트를 작성할 수 있게 해준다. ViewModel 인스턴스는 LiveData 인스턴스와 같은 LifecycleObservers를 포함할 수 있다. 하지만, ViewModel 인스턴스는 LiveData같이 라이프사이클 기반의 Observable 클래스의 변화를 관찰해서는 안된다. 만약, ViewModel이 시스템 서비스를 찾는 등의 이유로 Application Context가 필요하다면, AndroidViewModel 클래스를 상속받아서 생성자에서 Application 객체를 받도록 구현할 수 있다.

ViewModel의 생명주기

ViewModel의 유효 스코프는 ViewModel 인스턴스를 얻기 위해 ViewModelProvider에게 전달된 Lifecycle로 지정된다. ViewModel 인스턴스는 Lifecycle의 유효스코프가 완전이 끝날때 까지, 메모리에 남아있게 된다. Activity의 경우라면 finish되었을 때이며, Fragment라면 Activity로부터 detach되었을 때 이다.
아래의 그림은 Activity가 화면 회전이 되고, 종료되기 까지의 라이프사이클 상태를 설명하고 있다. 이 그림에서 Activity의 라이프사이클에 연관되어 ViewModel 인스턴스의 생존시간도 확인 할 수 있다. Fragment의 기본 생명주기에 따른 ViewModel의 생존시간도 이와 동일하다.
viewmodel-lifecycle.png
일반적으로 개발자는 ViewModel 인스턴스를 Activity의 첫 시작점인 onCreate()에서 요청할 것이고 onCreate()메서드는 상황에 따라 여러번 호출될 수도 있지만, ViewModel은 최초 요청으로부터 Activity가 소멸될 때까지 메모리에 유지된다.

Fragment간 데이터 공유하기

하나의 Activity안에서 2개 이상의 Fragment 간에 데이터를 주고받으며 소통하는 것이 흔한 경우이다. 에를들어 마스터-디테일 Fragment 구조에서는 마스터에서 목록이 표시되며, 디테일에서는 마스터에서 선택된 항목의 상세 내용이 표시되어야 한다. 이를 구현하려면 각 Fragment는 인터페이스를 구성해야 하며, Activity가 그 인터페이스를 모두 바인드해야 한다. 또한, 모든 Fragment가 상대 Fragment가 생성이 안되었거나 안보이는 등의 예외케이스에 대한 방어처리가 되어있어야 한다.
이러한 흔한 문제들은 ViewModel 을 사용하여 해결할 수 있다. 다음의 예제코드처럼 Fragment들이 Activity 스코프의 ViewModel을 서로 공유하도록 구현하면 된다.
public class SharedViewModel extends ViewModel {
    private final MutableLiveData<Item> selected = new MutableLiveData<Item>();

    public void select(Item item) {
        selected.setValue(item);
    }

    public LiveData<Item> getSelected() {
        return selected;
    }
}

public class MasterFragment extends Fragment {
    private SharedViewModel model;
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
        itemSelector.setOnClickListener(item -> {
            model.select(item);
        });
    }
}

public class DetailFragment extends Fragment {
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        SharedViewModel model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
        model.getSelected().observe(this, { item ->
           // Update the UI.
        });
    }
}
ViewModelProvider를 얻을 때, 두 프레그먼트 모두 getActivity() 메서드를 이용하고 있음을 주의한다. 같은 Activity를 이용하여 같은 ViewModel 객체를 요청하므로, 동일한 인스턴스가 얻어질 것이다.
이러한 접근방법은 다음과 같은 이점이 있다.
  • Activity가 각 Fragment간 데이터 전달시에 추가적인 작업을 할 필요가 없다.
  • 각 Fragment는 ViewModel 외에 다른 객체나 상태에 대해 더 알 필요가 없다. 그러므로 다른 Fragment가 사라지더라도 정상적으로 동작할 것이다.
  • 각 Fragment는 다른 Fragment의 라이프사이클을 신경쓰지 않고, 자신의 라이프사이클대로 작업을 수행할 수 있다.

ViewModel로 Loader 대체하기

CursorLoader 같은 Loader 클래스들은 UI에서 데이터베이스와의 싱크를 유지한 채 데이터를 가지고있기 위해 사용했었다. ViewModel과 몇몇 추가 클래스들을 이용하여 Loader를 대체할 수 있다. ViewModel을 사용하면 UI 컨트롤러와 데이터 로딩 작업을 분리할 수 있다.
Loader를 이용했던 흔한 접근방법중에 하나로, 앱은 데이터베이스의 정보를 관찰하기 위해 CursorLoader를 이용했었다. 데이터베이스에서 값이 변경되면, Loader는 관련 데이터를 자동적으로 가져온 후에 UI를 갱신하는 역할을 담당했다.
viewmodel-loader.png
ViewModelRoomLiveData를 이용하여 Loader를 대체할 수 있다. ViewModel을 이용하여, 디바이스의 구성이 변경되어도 데이터가 살아남도록 할 수 있고, Room은 데이터베이스의 변화가 있을 때 LiveData에 알려주면, LiveData는 새로 받아온 데이터를 이용해 UI를 갱신한다.
viewmodel-replace-loader.png

원문 : https://developer.android.com/topic/libraries/architecture/viewmodel.html#loaders


2018년 1월 15일 월요일

[안드로이드] 스레드3 - 기본 스레드의 생명주기 관리

기본 스레드의 관리

  • 안드로이드에서도 자바와 마찬가지로 java.lang.Thread 클래스가 모든 스레드의 근간이 된다.

1.기본개념

생명주기

  • Thread.State enum에 정의되어 있으며, Thread 인스턴스의 getStatus() 메서드를 이용해 확인할 수 있다.

NEW

  • Thread 클래스의 인스턴스를 생성한 직후의 상태
  • Thread 인스턴스 생성이 다른 클래스의 인스턴스 생성보다 무겁지는 않다.
  • 생성된 스레드는 현재 스레드와 동일한 우선순위의 스레드그룹으로 할당된다.
    • ex) UI스레드에서 생성하면 UI스레드와 동일한 우선순위의 스레드그룹이 된다.

RUNNABLE

  • Thread.start() 메서드가 호출되어 실행환경이 설정된 직후부터 스레드가 실행되는 중의 상태
  • 이후에 OS의 스케쥴러가 스레드를 선택하면 run() 메서드가 호출되어 태스크가 실행된다.

BLOCK / WAITING / TIMED_WAITING

  • I/O 동작, 다른스레드와의 자원동기화, 블로킹 API호출 등 스레드의 실행이 멈출 때의 상태들
  • 스레드를 명시적으로 실행을 멈추는 방법
    • Thread.sleep() : 스레드를 일정시간 동안 자도록 만든후 다시 실행하도록 스케쥴되도록 함
    • Thread.yield() : 현재 실행을 포기하고 스케줄러가 어떤 스레드를 실행할지 결정하도록 함

TERMINATED

  • run() 메서드가 실행을 완료하면 스레드가 종료되고 스레드의 자원이 해제된 후의 상태
  • 스레드가 종료되고 나면 해당 스레드의 인스턴스와 실행환경은 재사용할 수 없다.
  • 실행환경을 설정/해제하는 것은 매우 무거운 동작
  • 스레드가 동작을 명시적으로 종료하도록 요청할 수도 있다. - interrupt() 메서드 호출

인터럽트

  • 스레드를 명시적으로 종료하고 싶을 때, 스레드에 interrupt() 메서드를 이용해 인터럽트를 요청
  • 어디까지나 요청일 뿐, 인터럽트 호출에 대한 실행여부는 스레드 자체가 결정한다.
  • 일반적으로 스레드의 인터럽트는 공동으로(Collaboratively) 구현되어야 한다.
class TestThread extends Thread {
    @Override
    public void run() {
        //isInterrupt() 메서드를 수시로 검사하여 true를 반환하면 태스크를 완료한 것으로 처리
        while(!isInterrunpted()) {
            //스레드가 살아있음
        }
        //태스크 완료되어 스레드 종료됨
    }
}
  • 스레드 내에서 블로킹 API를 사용중이라 스레드가 차단되어있는 중에 인터럽트 신호를 받으면 블로킹 API는 InterruptException 을 던진다.
    • InterruptException이 던져지면 인터럽트 플래그가 리셋되므로 주의한다.
class TestThread extends Thread {
    @Override
    public void run() {
        //isInterrupt() 메서드를 수시로 검사하여 true를 반환하면 태스크를 완료한 것으로 처리
        while(!isInterrunpted()) {
            doBlockingAction();
        }
    }
    
    public void doBlockingAction() {
        try {
            //Blocking API 호출
        } catch (InterruptException e) {
            //1.자원 정리
            //2.isInterrupt() 플래그가 초기화되었으므로, run() 메서드가 isInterrupt()를 제대로 인식하도록 자신을 다시 인터럽트
            Thread.currentThread().interrupt();
        }
    }
}

잡히지 않는 예외

  • 모든 스레드는 RuntimeException이 발생했을 때, 이 예외를 처리해주지 않으면 스레드가 비정상적으로 종료된다.
  • 스레드 내에서 발생하는 RuntimeException을 잡으려면, 스레드가 비정상 종료되기 전에 호출되는 UncaughtExceptionHandler를 부착한다.
    • 모든 스레드에 전역 핸들러를 부착할 경우 : Thread 클래스의 setDefaultUncaughtExceptionHandler() 정적 메서드 호출
    • 특정 스레드에 지역 핸들러를 부착할 경우 : Thread 인스턴스의 setUncaughtExceptionHandler() 메서드 호출
    • 위의 두가지 경우가 모두 부착되어 있을 경우에는 특정 스레드마다 붙은 핸들러가 우선시되어 모든스레드에 부착된 핸들러는 호출되지 않는다.
  • 안드로이드 런타임은 앱이 시작될 때 전역 핸들러를 부착한다. 이 전역 핸들러의 기본동작은 잡히지 않은 예외가 발생했을 때, 프로세스를 죽이는 것으로 모든 스레드가 동등하게 처리된다.
  • Ex - 앱이 크래시되기 직전에 로그남기기
// 앱 시작시 실행되는 아무 클래스
public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate(0;
        //새로운 전역 핸들러를 설정
        Thread.setDefaultUncaughtExceptionHandler(new LogExceptionHandler());
    }
}

//새로 정의한 예외핸들러 클래스
public class LogExceptionHandler implements Thread.UncaughtExceptionHandler {
    private final Thread.UncaughtExceptionHandler defaultHandler;
    
    public LogExceptionHandler() {
        defaultHandler = Thread.getDefaultUncaughtExceptionHandler();
    }
    
    @Override
    public void uncaughtException(Thread thread, Throwable throwable) {
        //로그를 파일로 남기거나 서버로 전송
        
        //안드로이드 런타임이 붙였던 기존 핸들러로 작업 위임
        default.Handler.uncaughtException(thread, throwable);
    }
}

2.스레드 관리

스레드 정의(Define)와 시작

  • 스레드 정의 방법에 따라, 그 스레드의 생명주기가 다르다. 그래서 때때로 안드로이드의 컴포넌트(Activity, Service, BroadcastReceiver, ContentProvider)와 생명주기가 달라서 메모리 누수를 일으키기도 한다.

익명 내부클래스로 정의

  • 익명 내부클래스는 구현이 간단하지만, 외부클래스의 인스턴스에 대한 참조를 내부적으로 유지하고 있다.
public class AnyObject {
    //UI 스레드에서 호출되는 메서드
    @UiThread
    public void anyMethod() {
        new Thread() {
            @Override
            public void run() {
                // 긴 태스크를 실행한다고 가정
                // 해당 스레드가 살아있는 동안 이 스레드는 AnyObject 인스턴스의 참조를 유지하고 있다.
                doLongRunningTask();
            }
        }.start();
    }
}

공개된 독립 클래스로 정의

  • 스레드를 실행하는 인스턴스에 대한 잠재적인 참조를 유지하지는 않지만, 클래스의 개수가 많아진다.
class TestThread extends Thread {
    @Override
    public void run() {
        // 긴 태스크를 실행한다고 가정
        doLongRunningTask();
    }
}

public class AnyObject {
    //UI 스레드에서 호출되는 메서드
    private TestThread testThread;
    
    @UiThread
    public void anyMethod() {
        testThread = new TestThread();
        testThread.start();
    }
}

정적 내부 클래스로 정의

  • 외부 클래스의 클래스 객체에 대한 내부참조를 유지하고 있다. 그렇지만 클래스객체의 참조는 메모리 누수의 원인이 되지 않는다.
public class AnyObject {
    static class TestThread extends Thread {
        @Override
        public void run() {
            // 긴 태스크를 실행한다고 가정
            doLongRunningTask();
        }
    }

    private TestThread testThread;

    //UI 스레드에서 호출되는 메서드
    @UiThread
    public void anyMethod() {
        testThread = new TestThread();
        testThread.start();
    }
}

스레드 유지

  • Activity의 설정이 변경되면 기본적으로 Activity는 재시작되며 가지고있던 멤버변수들도 초기화된다. - 당연히 스레드도 초기화되어 새로운 객체로 생성되어 다시 시작된다.
  • 해당 증상을 방지하기 위해 Activity와 Fragment는 설정의 변경에도 객체를 유지할 수 있는 방법을 제공한다.

Activity에서 스레드 유지

  • pulbic Object onRetainNonConfigurationInstance()
    • 설정 변경이 일어나기 전에 플랫폼에 의해 호출되는 콜백메서드
    • 새로 생성되는 Activity 객체에 전달할 객체를 반환하도록 구현해야 한다.
  • public Object getLastNonConfigurationInstance()
    • 설정 변경이 이루어진 후, 새로 생성된 Activity 객체에서 호출하는 메서드
    • onRetainNonConfigurationInstance() 메서드에서 반환한 객체를 반환한다.
    • 설정 변경이 아닌 다른이유로 Activity가 재시작 될 경우 null을 반환한다.
  • 예제
public class TestActivity extends Activity {
    private static class TestThread extends Thread {
        @Override
        public void run() {
            //TODO::긴 작업
        }
    }

    private static TestThread thread;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout_activity_test);
        Object retainedObject = getLastNonConfigurationInstance();
        if (retainedObject != null) {
            thread = retainedObject;
        } else {
            thread = new TestThread();
            thread.start();
        }
    }
    
    @Override
    public Object onRetainNonConfigurationInstance() {
        if (thread != null && thread.isAlive()) {
            return thread;
        }
        return null;
    }
}

Fragment에서 스레드 유지

  • Fragment의 상태 유지요청 메서드인 setRetainInstance(true)를 이용하여 프레그먼트를 유지한다.
  • 예제
public class TestFragment extends Fragment {
    private static class TestThread extends Thread {
        @Override
        public void run() {
            //TODO::긴 작업
        }
    }

    private TestThread thread;
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (thread == null) {
            thread = new Thread();
            thread.start();
        }
        // 이 메서드만 설정해주면 프레그먼트는 엑티비티의 설정이 변경되는 동안 객체가 유지된다.
        setRetainInstance(true);
    }
}

public class TestActivity extends Activity {
    TestFragment fragment;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout_activity_test);
        FragmentManager manager = getFragmentManager();
        fragment = manager.findFragmentByTag("TestFragment");
        if (fragment == null) {
            FragmentTransaction transaction = manager.beginTransaction();
            fragment = new Fragment();
            transaction.add(fragment, "TestFragment");
            transaction.commit();
           
        }
    }
}