프래그먼트: 안드로이드의 모든 문제의 해결책이자 원인

Context

태초에 액티비티가 있었습니다. 그리고 잠시 동안은 상황이 좋았습니다. 안드로이드 3.0 전까지는 액티비티가 본질적으로 애플리케이션에서 발생하는 대부분의 일을 감독하는 전능한 객체였습니다.

하지만 2011년에 태블릿이 시장에 등장하고나서부터 개발자는 비즈니스 로직을 복사하지 않고도 더 큰 화면 크기를 지원하기 위해 코드를 재사용할 방법이 필요해졌습니다. 그 대표적인 예가 마스터/디테일 플로우입니다. 따라서 안드로이드 팀은 이처럼 단일 액티비티가 거대해지는 것을 막기 위해 프래그먼트나 조합 가능한 액티비티를 추가했습니다.

하지만 이 추가 기능들은 열렬히 환영받지는 못했습니다.

초급 개발자뿐만 아니라 숙련된 개발자들에게도 프래그먼트는 혼란스러운 것이었습니다. 게다가 Google은 사용 목적에 대한 가이드를 그다지 제공하지 않았는데, 이는 어느 정도 의도된 것이었습니다.

2016 Google I/O에서 안드로이드 프레임워크 팀의 Adam Powell은 어떻게 효과적으로 프래그먼트를 사용할 수 있는지에 대해 강연했습니다. 이 강연에서 Adam은 개발자가 프래그먼트를 사용하면서 자주 저지르는 실수에 대해 말했습니다.

  • 복잡한 생명주기 - 액티비티 생명주기는 복잡하지만, 프래그먼트를 섞기 시작하면 결과는 위협적이기까지 합니다.

  • 프래그먼트 매니저 트랜잭션 - 비동기로 인해 예기치 않은 동작이 발생할 수 있습니다. (또한, 버그도 많을 수 있습니다. 트랜잭션 내에서 문제가 발생했다면 안드로이드는 디버그 하기 정말 어려운 무시무시한 IllegalStateException을 던질 것입니다.

  • 커스텀 뷰를 쓸 것인가 프래그먼트를 쓸 것인가? - 커스텀 뷰를 쓸지 프래그먼트를 쓸지에 대한 혼동이 있습니다. Fragment Tag를 봐도 도움이 되질 않죠.

Adam의 강연에 따르면 프래그먼트를 여러 다른 방식으로 생각하면 이런 대부분의 문제를 극복할 수 있다고 합니다.

첫째로, 다른 안드로이드 구성 요소와 마찬가지로 프래그먼트는 애플리케이션에 간단하게 진입할 수 있는 지점입니다. 사실 추상화 측면에서는 액티비티와 동일합니다. 프래그먼트를 더 작고 똑똑하게 작성할수록 주변 환경에 덜 영향을 받게 됩니다.

둘째로, 프래그먼트는 예쁜 뷰가 아닙니다. 프래그먼트는 UI를 구현하기 위해 뷰를 사용하지만 뷰를 사용하지 않는 프래그먼트를 가질 수 있고, 이렇게 사용해도 아무 문제가 없습니다. 하지만 실제로 프래그먼트는 뷰에 의존하고 뷰 내에서 발생하는 이벤트에 응답해야 합니다. 한편 뷰는 프래그먼트에 대한 지식이 없어야 합니다.

프래그먼트는 다른 안드로이드 구성 요소 사이를 조정하고 애플리케이션 비즈니스 로직을 집행해야 합니다.

하지만 프래그먼트를 도입하려고 노력하던 몇 년 동안 개발자들은 불안해했고 대안을 갈구하게 됐습니다.

프래그먼트 없는 아키텍처 대두

Square - 프래그먼트 반대

이런 개발 뉴스를 더 만나보세요

안드로이드 프레임워크에 프래그먼트가 추가된 지 약 삼 년 이후인 2014년에 Square의 개발자들이 프래그먼트를 반대하는 블로그 글을 작성했습니다. Square는 앞서 논의한 프래그먼트의 문제 외에도 프래그먼트 매니저 디버깅 문제와 프래그먼트가 재생성될 때 발생할 수 있는 예외에 대해서도 지적했습니다.

Square는 프래그먼트의 결함에 대해 단지 지적만 한 것이 아니라, 프래그먼트 없이 애플리케이션을 쉽게 만들 수 있는 도구, Flow와 Mortar를 만들었습니다.

Flow는 간단한 POJO 클래스를 사용해서 “스크린”이라는 UI를 만들 수 있게 하는 라이브러리입니다. 스크린에 필요한 정보는 연관될 뷰와, 뷰를 업데이트하는 데 필요한 메서드뿐입니다. 또한 Flow는 액티비티와 프래그먼트 매니저 백스택에 독립적이고 정말 간단한 기본적인 네비게이션 스택을 제공합니다. 이를 통해 화면 간에 이동할 수 있고 스크린 간에 이동하면서 상태를 탐색하고 유지할 수 있습니다.

Flow가 UI를 캡슐화하고 네비게이션 스택을 제공한다면 비즈니스 로직은 어디에 둬야 할까요? 직접 라이프사이클을 관리해야 할까요? 여기서 Mortar가 등장합니다. Mortar는 MVP 패턴을 사용해서 비즈니스 로직을 프리젠터로 감싸고 뷰와 상호작용하는 라이브러리입니다. 각 프리젠터는 Dagger로 범위가 지정되므로 해당 리소스는 프리젠터가 사라지는 경우 정리됩니다. 또한 Mortar는 프리젠터를 관리하기 위해 매우 긴 생명주기를 갖습니다.

Flow와 Mortar를 사용하면 많은 장점이 있습니다.

  • 프레그먼트를 사용하지 않음

  • MVP와 의존성 주입을 통해 모듈화 되고 테스트가 가능한 코드 강제

  • Dagger 범위로 인해 메모리 효율성 증대

다만 아쉽게도 이 블로그를 작성하는 시점에는 몇 가지 큰 단점이 있습니다.

  • 학습 곡선이 매우 가파름 - Dagger/Dagger 2나 MVP 패턴에 익숙하지 않은 경우 암벽 등산을 하는 기분을 느낄 수도 있습니다.

  • 많은 상용구 코드

  • 두 라이브러리 모두 매우 긴 릴리즈 주기

    • Mortar의 경우 마지막 마이너 업데이트 이후 일 년 이상 경과

    • Flow는 2016년 2월에 1.0.0-alpha에 도달했고, 9월에 Flow 1.0.0-alpha2 발표

Square의 블로그 글과 라이브러리는 안드로이드 커뮤니티의 논쟁에 불을 붙였습니다. 해당 글 때문에 두 개의 커뮤니티가 즉각 생성됐습니다. 디자인 패턴을 사용해서 프래그먼트를 사용하기 쉽게 만들고자 하는 사람들과 다시는 프래그먼트를 사용하지 않겠다는 사람들로 나뉜 것이죠. 먼저 반 프래그먼트 커뮤니티를 살펴보겠습니다.

Down With Fragments

프래그먼트를 사용해서 애플리케이션을 개발하는 데 가장 유명한 패턴은 프래그먼트 네비게이션 패턴, 혹은 “One-Activity-Multiple-Fragments” 아키텍처입니다. 만약 이 패턴을 사용한 앱이라면 여러 액티비티를 사용하도록 전환할 수 있습니다. 이 패턴을 사용하지 않고 프래그먼트를 없애고자 한다면 커스텀 뷰를 고려할 수도 있습니다. 하지만 이 두 접근 방법 모두 어려움이 있습니다.

여러 액티비티로 전환하는 경우 특별하게 살펴야 할 부분은 타블렛과 같은 다양한 폼 팩터를 지원하기 위해 인터페이스를 최적화할 방법입니다. 안드로이드는 특정 화면 크기에 대해 리소스를 한정하는 방법을 제공하므로 간단한 레이아웃에서는 잘 작동합니다. 그러나 장치마다 다른 경험을 제공하기 위해 마스터-디테일 인터페이스를 구현하고 싶다면 어떻게 해야할까요? 각 폼 팩터를 처리하거나 문제를 해결하기 위해 완전히 다른 액티비티를 작성해야 하므로 특별한 로직을 코딩해야 할 수 있습니다.

하지만 이런 경우 인터페이스가 유지 보수하기 어려워질 수 있습니다. 프래그먼트의 장점은 UI 구성요소를 분리하므로 액티비티가 다른 것에 집중할 수 있다는 점이죠. 커스텀 뷰가 같은 이점을 가질 수 있을까요?

간단히 대답하면 그렇습니다. UI의 상태를 걱정하지 않아도 된다면 말이죠. 액티비티와 익스텐션으로의 프래그먼트가 가지는 목적은 이들이 떠난 상태에 그대로 머무르도록 하기 위해서입니다. 커스텀 뷰를 사용하려면 몇 가지 부분을 고려해야 합니다.

  • 뷰 상태 방향이 바뀌는 경우, 뷰는 파괴되고 재생성됩니다. 뷰 상태를 저장하려면 각 커스텀 뷰의 onSaveInstanceState onRestoreInstanceState 메서드를 오버라이드하고 뷰 상태를 parcelable로 저장해야 합니다.

    뷰 상태를 관리할 때 다음 라이브러리를 사용하면 보다 간편해집니다. Icepick나, AutoValueParcelable extension을 같이 사용하는 것이죠. 이들 라이브러리의 목적은 Parcelable를 만드는데 필요한 상용구를 줄이는 것입니다.

  • 백스택 관리 백 버튼을 어떻게 관리해야 할까요? 사용 사례에 따라 프래그먼트를 사용해서 얻은 동작을 모사할 수 있습니다.

    예를 들어, 하나의 액티비티 안에서 두 뷰를 탐색하고 안드로이드 백 버튼이나 툴바를 사용해서 같은 상태로 돌아가려는 상황을 가정해 보겠습니다. 프래그먼트 매니저는 프래그먼트를 기록 스택에 푸시하고 수동으로 popbackstack을 호출해서 언제든 사용자가 백 버튼을 누르는 경우 이전 상태로 돌아갈 수 있게 합니다. 반면 뷰는 이와 유사한 구조가 없으므로 액티비티의 스택에 의존하거나 직접 솔루션을 구현해야 합니다.

    뷰 백스택 개념은 “Single Activity - Multiple Fragment” 아키텍처에서 “Single Activity - Multiple View” 아키텍처로 변경하는 경우 훨씬 더 어려워집니다.

Flow와 Mortar의 여파로 이런 상황에서 사용할 수 있는 몇몇 라이브러리가 등장했습니다.

Simple Stack

오랫동안 프래그먼트를 반대해온 Gabor Varadi프래그먼트 없이 생활하기와 관련된 주제로 광범위하게 글을 썼으며, 어떻게 Flow의 백스택이 동작하는지에 대한 글도 썼습니다. 2016년에 Gabor는 Square의 Flow 라이브러리를 포크해서 주목할만한 향상 및 수정을 추가한 Flowless를 만들었습니다. Flow 디자인의 근본적인 한계로 인해 Gabor는 궁극적으로 자체 백스택을 만들었죠.

그 결과 Simple-Stack 라이브러리가 탄생했습니다. Simple-Stack은 애플리케이션의 상태를 재현하고 탐색할 수 있는 백스택 라이브러리입니다. Backstack 클래스는 UI를 나타내는 간단한 Java parcelable 객체를 사용해서 UI를 추적합니다.

Flow와 같은 맥락에서 백스택의 goTo(<key>), goBack(), setHistory() 메서드를 호출해서 애플리케이션의 상태를 변경할 수 있습니다. 백스택은 BackstackManager를 통해 유지되며, 구성 변경이나 프로세스 종료 후에도 유지됩니다.

Simple-Stack 사용 방법에 대한 예시를 보고 싶다면 Gabor의 Master-Detail, Nested Stack 예제를 참고하세요.

Conductor

Conductor는 “뷰 기반의 안드로이드 애플리케이션을 만들 수 있는 작지만 완벽한 기능을 갖춘 프레임워크”라고 설명하는 라이브러리입니다. 어떤 이들은 이 라이브러리가 프래그먼트를 더 낫게 구현한다고 합니다.

프래그먼트처럼 Conductor는 아키텍처에 무관하게 사용할 수 있으므로 어느 MV* 패턴에서나 쓸 수 있습니다.

바로 사용할 수 있는 기능은 다음과 같습니다.

  • 뷰를 감싸고 더 단순한 생명주기에 접근할 수 있도록 하는 간단한 Java 클래스인 컨트롤러

  • 컨트롤러 간 이동이 가능한 라우터라고 불리는 백스택

  • 상태 저장, 컨트롤러는 구성이 변경되는 동안 유지

수명이 긴 프래그먼트

Square가 말했듯 특히 격리 상태에서 프래그먼트를 테스트하는 것은 어렵습니다. 하지만 Square는 Mortar를 통해 안드로이드 생명주기에서 비즈니스 로직과 뷰 코드를 분리하고 테스트 가능한 프리젠터를 만드는 방법을 선보였습니다. 비록 Square가 안드로이드에서 디자인 패턴을 처음으로 사용한 것은 아니지만 Square의 블로그 글 이후 안드로이드 커뮤니티는 이런 패턴을 보다 명료하고 테스트 가능한 코드로 활용하는 방법을 알게 되었습니다.

이들 디자인 패턴을 학습하기 위한 많은 자료가 있습니다.

더 똑똑한 프래그먼트

프래그먼트에 대한 Adam의 강연 중, 그는 프래그먼트가 더 작고 독립적일수록 사용이 쉬워진다고 말했습니다. 그리고 우리는 Square가 Flow와 Mortar로 달성한 것들과 디자인 패턴으로 어떻게 코드를 테스트할 수 있는지도 살펴봤죠. 아쉽게도 새로운 애플리케이션으로 작업하지 않는 이상, 혹은 유지보수 예산이 넘쳐 흐르지 않는 이상, 프래그먼트를 사용하는 기존 애플리케이션을 유지 관리하게 되는 경우가 많습니다.

라이브러리나 구성 요소가 등장해서 프래그먼트를 완벽하게 교체하는 날이 오고 기존 애플리케이션을 이에 맞게 다시 고칠 예산까지 생기는 꿈같은 날이 오기 전까지는, 어쩔 수 없이 프래그먼트를 보다 쉽게 작업하고 테스트 가능성과 적용 범위를 늘릴 방법을 찾아야 합니다. 아이디어는 비즈니스 코드를 안드로이드 코드에서 분리 이동해서 프래그먼트를 더 똑똑하게 하는 것입니다. 그다음 비즈니스 코드를 단위 테스트해서 애플리케이션 내에서의 코드 커버리지를 강화할 수 있습니다.

예를 들어 저라는 특정 사용자(myotive)의 GitHub 저장소의 목록을 보여주는 애플리케이션을 만든다고 생각해 보겠습니다. 전체 코드는 여기에서 볼 수 있습니다. 이 코드는 Google 아키텍처 Todo MVP Dagger 예제에서 영감을 받았습니다.

MVP를 활용해서 모델(GitHub API)과 상호작용하는 프리젠터와 뷰 인터페이스를 만들었습니다. 그리고 뷰 인터페이스를 구현해서 UI 업데이트를 처리하는 프래그먼트가 있습니다.

public class RepositoryContract {
    public interface Presenter<T extends BaseView> extends BasePresenter<View>{
        void getRepositories();
    }

    public interface View extends BaseView {
        void updateRepositoryList(List<Repository> repositories);
        void onRepositoryItemClick(Repository item);
    }
}

Dagger 2 의존성 주입 프래그먼트를 사용해서 프리젠터를 프래그먼트로 직접 주입할 수 있습니다.

public class RepositoryFragment extends Fragment implements 
           RepositoryContract.View
{
    @Inject
    RepositoryContract.Presenter presenter;
    
    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {

        View view = inflater.inflate(R.layout.fragment_repository, container, false);
        
        // obtain Dagger 2 ActivityComponent from Actiity
        ActivityComponent activityComponent = ((MainActivity)getActivity()).getActivityComponent();
        // Inject dependencies
        activityComponent.inject(this);
        
        presenter.setView(this);
        ...
        return view;
    }
}

프리젠터를 프레그먼트에 주입해서 프리젠터와 뷰 로직을 분리했으므로 어떤 유닛 테스팅 프레임워크를 사용하더라도 각각을 프레그먼트와 독립적으로 테스트할 수 있습니다. 이를 통해 프래그먼트 내에서 코드 커버리지가 더 높아집니다.

public class RepositoryUnitTest {
    @Mock
    private RepositoryContract.View view;
    @Mock
    private ProgressBarProvider progressBarProvider;
    @Mock
    private GitHubAPI gitHubAPI;
    @Mock
    private Call<List<Repository>> mockCall;

    @Captor
    private ArgumentCaptor<Callback<List<Repository>>> captor;

    private RepositoryPresenter presenter;

    @Before
    public void setup(){
        MockitoAnnotations.initMocks(this);

        presenter = new RepositoryPresenter(gitHubAPI, progressBarProvider);
        presenter.setView(view);
    }

    @Test
    public void test_getRepositories() {
        // arrange
        String user = BuildConfig.GITHUB_OWNER;
        List<Repository> repositories = new ArrayList<>();
        repositories.add(new Repository());

        when(gitHubAPI.GetRepos(user)).thenReturn(mockCall);
        Response<List<Repository>> response = Response.success(repositories);

        // act
        presenter.start();

        // assert
        verify(mockCall).enqueue(captor.capture());
        captor.getValue().onResponse(null, response);

        verify(progressBarProvider).showProgressBar();
        verify(progressBarProvider).hideProgressBar();

        verify(view).updateRepositoryList(repositories);
    }
}

결론

프래그먼트는 완벽하지는 않지만, 안드로이드 프레임워크에 추가된 이후에 많은 발전이 있었습니다. 2017년 현재, 프래그먼트와 관련해서 커뮤니티는 Square나 Gabor와 같은 여러 개발자의 노력 덕분에 교차 선상에 설 수 있었습니다. 양측간의 건강한 토론과 다른 관점을 통해 안드로이드 개발 경험이 향상될 수 있었죠.

프래그먼트는 아직 UI를 재사용하고 단일 액티비티를 분리하며, 안드로이드 생명주기 내에서 동작하는 구조를 제공합니다. 현재 프래그먼트 트랜잭션과 자식 프래그먼트와 관계된 많은 버그가 수정되었습니다. 또한, 비즈니스 코드를 프리젠터와 같은 테스트 가능한 구조로 옮겨서 프래그먼트를 더욱 똑똑하게 사용하도록 할 수 있습니다. 따라서 상태 복원 등 프레임워크를 다루는 문제가 점점 해소되고 있습니다.

하지만 프래그먼트를 정말 사용하기 싫다면 2017년에는 다른 옵션도 있습니다. 앞서 소개한 Simple-StackConductor를 시도해 보세요. 두 라이브러리 모두 뷰 기반의 유연한 애플리케이션을 만드는 데 도움을 줄 겁니다.

다음: 안드로이드 아키텍처 #7: 잘 짜여진 실제 앱 해부해보기: 안드로이드 리버스 엔지니어링

General link arrow white

컨텐츠에 대하여

이 컨텐츠는 저자의 허가 하에 이곳에서 공유합니다.


Michael Yotive

Michael은 오하이오주 더블린에 위치한 Startup의 Healthy Roster, Inc.의 소프트웨어 개발자입니다. 지난 10년간 전문적으로 프로그래밍을 해왔으며, 취미는 음악 감상과 크로스핏입니다.

4 design patterns for a RESTless mobile integration »

close