Sfandroid fabien devos facebook

액티비티와 프래그먼트에서 벗어나 간결하게 Android 앱 만들기

액티비티는 화면을 회전할 때마다 파괴될 뿐만 아니라 생명주기가 너무 복잡합니다. 한편 프래그먼트를 사용하면 에러가 발생하기 쉽고, 생명주기도 한층 더 복잡해지죠. 회전을 해도 파괴되지 않는 화면을 만들고 goTo(screen) 처럼 간단한 호출로 내비게이션하려면 어떻게 할까요? 간결하고 세련된 최신 Android 개발 방법, 모던 안드로이드를 알려드립니다.


소개 (0:00)

저는 Wealthfront의 안드로이드 리드 개발자 Fabien입니다. 이번 포스트에서는 어떻게 저희 Wealthfront 팀이 예전 스타일의 뷰를 제가 “모던 안드로이드”라고 부르는 최신 개발 방법으로 바꿨는지 말씀드리겠습니다.

생명주기 (0:50)

생명주기는 안드로이드 개발의 기본 원리입니다. 먼저, 한 액티비티가 생성되고 시작됩니다. 그 다음으로 재시작(resumed)되는데, 이는 뷰가 보이게 됐다는 뜻입니다. 멈춰질(paused) 수도 있는데 이 때는 부분적으로 보입니다. 그 다음 멈춘 후 파괴될 수 있죠.

Fabien-Android-Lifecycles

이런 설명이 이상적으로 보이지만, 실제 상황에서는 온갖 극한 케이스들이 발생하곤 합니다.

예를 들어 어떤 경우에는 onDestroy가 불리지 않은 채로, 새로운 액티비티를 파괴하는 과정에서 바로 죽을 수도 있습니다. onPauseonStop은 호출이 보장되지만 onDestroy는 그렇지 않습니다.

Modern-Android-Activity-lifecycle-real

더 나쁜 상황을 보여드리겠습니다. 극한 케이스에서도 선택적 메서드들이 다양하게 존재합니다.

게다가 우리에겐 심각하게 복잡한 프래그먼트도 있습니다. 어떤 극한 케이스에서는 예상대로 동작하지 않을 뿐만 아니라 변경되기까지 하죠. 심지어 생명주기는 안드로이드의 여러 버전에서 계속 바뀌어왔습니다.

Modern-Android-fragment-lifecycle

그래서 저는 Wealthfront에서 시작한 새 프로젝트에서 보다 간단한 것을 도입하기로 마음먹었습니다. 다른 방법을 시도하기로 한 겁니다. 처음 말씀드린 원래의 단순한 생명주기와 비슷한 모습으로 만들 수 있다면 좋지 않을까요?

문제점 (3:54)

복잡한 생명주기

생명주기 중 어떤 부분의 문제를 해결하기 위해 노력할 수는 있겠지만, 항상 여러 문제가 있다는 것을 이해해야 합니다.

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

객체 전달시 직렬화(serialization) 필요

앱에 따라 다르긴 하지만, 객체를 항상 직렬화해야 한다면 너무 불편합니다. 어떤 경우에는 이들을 메모리에 올렸다가 다음 화면에 넘길 수 있다면 좋겠죠.

회전 시 파괴

물론 회전할 때 액티비티가 파괴되는 문제를 풀기 위해 플래그 등을 설정할 수는 있지만, 그 방법 역시 문제를 만듭니다. 실제 해결책이 될 수는 없죠.

백스택 제어의 어려움

이 앱은 내 앱이니까 원하는 화면으로 바로 옮겨가야지.” 라거나, “이건 임시 화면이니까 백스택에는 넣지 않겠어” 라고 간단하게 제어할 수 있으면 좋겠지만 그렇지 않습니다. 사실 이런 종류의 일은 매우 간단해야 하는데도 실제로는 아니죠.

해결책 (5:42)

비슷한 해결책을 만들기 위해 많은 훌륭한 프로젝트가 진행 중이며, 개발자 커뮤니티 역시 이런 방향으로 발전하고 있습니다.

제가 제안하는 해결책은 이렇습니다.

  1. 단일 액티비티 구조
  2. 뷰에서 로직 분리
  3. 회전을 해도 화면만 그대로, 뷰는 파괴
  4. 스스로의 내비게이션 시스템 작성

저는 한 액티비티만 사용하고 뷰를 직접 다룰 생각입니다. 그러려면 좋은 구조를 만들고 뷰에서 로직을 분리하는 것이 중요합니다. 또한 화면은 회전을 해도 살아남지만 뷰는 그렇지 않게 할 예정입니다. 즉, 회전을 하면 로직과 데이터 부분은 그대로이지만, 뷰는 파괴된 후 제대로 된 크기로 맞춰져서 재생성되는 겁니다. 마지막으로 자신의 내비게이션 시스템을 작성해야 하는데, 이는 건너뛸 편법이 없는 필수적인 과정입니다.

단일 액티비티 구조 (7:12)

단일 액티비티 구조를 사용하면 간단하게 하나의 액티비티만 사용할 수 있습니다. 화면 A를 위한 뷰가 있을 때, 새로운 화면 B를 위해 새 액티비티를 만드는 것이 아니라 단순히 뷰를 바꾸기만 하면 됩니다.

Modern-Android-single-activity

화면과 뷰 (7:39)

제가 만든 구조에서는 화면을 앱의 로직 부분으로 언급하겠습니다. 모델-뷰-프리젠터(Model View Presenter : MVP) 패턴에서 영감을 얻었습니다. 모델과 뷰 사이에 프리젠터가 있는데, 모델은 프리젠터와, 프리젠터는 뷰와 소통하죠.

이는 오랜동안 보아왔던 양파(onion) 모델과 비슷합니다. 레이어를 사용해서 코드를 만들고, 각 레이어는 하단 혹은 상단의 레이어와만 소통합니다.

Modern-Android-MVP

제 모델에서는 데이터 스크린 뷰 패턴을 사용합니다. 가운데에 로직과 데이터가 위치하고, 이 데이터를 보여주기 위해 뷰가 존재합니다.

로직이 스크린에 있다는 것은 뷰가 큰 역할을 하지 않는다는 뜻입니다. 다시 말하면 뷰는 로직없이 멍청합니다. 뷰에 로직을 넣는다면 단일 책임 원칙과 책임 분리 원칙을 위배하게 됩니다. 이 모델을 작동하는데 가장 중요한 부분이 바로 뷰를 멍청하게 두는 겁니다.

일반 Java 객체로 화면을 구성할 수 있기 때문에 화면이 테스트하기 쉬워집니다. 안드로이드 프레임워크에 종속되지 않는 단순한 Java 객체이므로 인스턴스를 만들고 테스트할 수 있죠.

또한 종속성 주입을 사용하는 경우에 화면이 이를 쉽게 받을 수 있는 부가효과도 있습니다.

내비게이션 (11:04)

Wealthfront의 시스템을 설계할 때 가장 중요하게 생각한 목표는 다음과 같습니다. 백스택을 완전히 제어할 수 있고, 애니메이션을 자동으로 처리할 수 있으며, 액티비티 생명주기에 화면을 연결할 수 있게 하는 것입니다.

단일 액티비티 시스템을 원한 이유는 액티비티를 완전히 버릴 수는 없었기 때문입니다. 화면에 뭔가를 그리고 다른 모든 안드로이드 시스템과 통합하기 위해서는 액티비티가 필요했습니다. 하지만 동시에 화면을 라이프사이클에 연결하고 싶었죠.

goTo(thisScreen)을 호출하는 것만큼 간단한 내비게이션을 만들기를 원했습니다. 얼마나 어려운 작업일까요?

주 목표가 내비게이터, 즉 항해사를 만드는 것이므로 이 프로젝트를 Magellan 프로젝트라고 명명했습니다. 내비게이터의 역할은 화면들을 추적하고 뷰의 애니메이션을 적용하는 것입니다.

Modern-Android-rotation

기기를 회전할 때 화면은 살아남지만, 뷰는 그렇지 않습니다. 내비게이터는 화면의 스택을 가지고 있고 메인 액티비티와 연결됩니다. 내비게이터는 메인 액티비티에 살아있는 화면에 요청해서 뷰를 가져옵니다. 회전을 하면 메인 액티비티가 시스템에 의해 파괴되고, 뷰 역시 파괴되었다가 다시 그려집니다.

Modern-Android-navigation

하지만 화면은 살아남죠. 새 메인 액티비티와 새 뷰가 생기지만, 새 화면이 생기지는 않습니다. 한 화면에서 다른 화면으로 이동하기 위해 스택에 넣게 되는데, 내비게이터에서 새 화면을 넣습니다. 이 새 화면을 통해 새 뷰를 가져올 수 있죠. 뷰를 가져와서 멋진 트랜지션 효과를 넣고 다른 뷰로 교체합니다. 다시 뒤로 돌아가고 싶다면 스택의 현재 제일 위에 있는 화면에 뷰를 요청하면 됩니다.

이는 단지 이론 상의 것이 아니라 실제로 상용 앱에서 이미 사용하고 있는 구조입니다.


navigator.goTo(new MySimpleScreen(stuff));

일반 생성자가 있는 일반 Java 객체라는 것을 눈치채셨나요? 다시 말해, 객체에 대한 참조를 전달할 수 있습니다. 평범한 Java를 사용해서 화면에 필요한 데이터를 직접 전달할 수 있습니다.

더 화려한 효과를 원한다면 트랜지션을 오버라이드할 수도 있습니다. 이 방식으로 원하는 것은 뭐든 할 수 있죠.


navigator.overrideTransition(transition);

내비게이터를 얻으려면 루트 화면을 설정합니다. 항상 한 화면이 스택에 있어야 하며, 없는 경우 내비게이터 예외가 발생하므로 API에서 있도록 강제합니다.


Navigator
.withRoot(new HomeScreen())
.loggingEnabled(BuildConfig.DEBUG)
.build();

여러 메서드가 있는데, 특정 화면으로 돌아가려면 goBack을 호출합니다.


navigator.goBack();
navigator.goBackTo(screen);

화면을 교체할 수도 있습니다.


navigator.replace(screen);

사용자가 아직 로그인하지 않았다면 로그인 화면이 필요하겠죠? 만약 이미 로그인했다면 홈 스크린을 띄울 겁니다.


navigator.rewriteHistory(new HistoryRewriter() {
	@Override
	public void rewriteHistory(Deque<Screen> history) {
		if (!authenticator.isUserLoggedIn()) {
			history.clear();
			history.push(loginScreen);
		} else {
			history.push(homeScreen);
		}
	}
	}
);

화면을 어떻게 구현하면 좋을까요?


public class MySimpleScreen extends Screen<MySimpleView> {
	@Override
	protected MySimpleView createView(Context context) {
		return new MySimpleView(context);
	}
	@Override
	public String getTitle(Context context) {
		return context.getString(R.string.my_screen_title);
	}
	@Override
	protected void onViewCreated() {
		getView().displayStuff(stuff);
	}
}

Screen을 상속받고 안전한 형 사용을 위해 뷰 타입을 취합니다.

인스턴스화할 수 있고 원하는 곳 어디든 보낼 수 있는 뷰를 만듭니다. 여러 유틸리티도 함께 포함되는데요, 예를 들어 getTitle은 액션 바에 제목을 자동으로 넣어줍니다.

물론 액티비티의 생명주기에 아직 결합되긴 하지만, onResume이나 onPause처럼 정말 필요한 메서드에만 국한되도록 했습니다.


public class MySimpleScreen extends Screen<MySimpleView> {

	@Override
	protected void onResume(Context context) {

	}

	@Override
	public String onPause(Context context) {

	}

	...
}

onResumeonPause가 실제로 필요한 시점에 호출되므로 한 화면에서 다른 화면으로 넘어갈 때 실제 onResume이 액티비티에서 호출되지 않는 경우라도 이들이 잘 호출됩니다.

구현 (18:18)

사실 우리가 여태까지 본 것은 단지 API였고, 실제 구현은 보다 까다로운 부분이긴 합니다. 여러분을 위한 팁을 공유하겠습니다.

내비게이터는 한 개만 사용하기

내비게이터를 다수 만드는 것은 좋지 않습니다. 싱글턴으로 내비게이터를 주입하거나 내비게이터를 항상 갖기 위해 onRetainNonConfigurationInstance()를 사용하세요. 이 모델이 잘 작동하기 위해서는 내비게이터가 살아 있어야 합니다. 만약 내비게이터가 액티비티와 함께 파괴된다면 모델 사용 목적이 무용지물이 돼버리죠.

백스택에는 간단한 Dequeue(Stack) 사용하기

이 방법으로 pop과 push가 가능합니다.

전환 중에는 사용자의 백스택 변경 막기

이 이유 때문에 작업 히스토리를 건드리는 것은 까다롭고 기본적으로 좋은 방법이 아닙니다. 사용자가 시스템의 균형을 깨뜨리는 일을 하길 바라는 분은 없겠죠?

항상 생명주기 균형 유지하기

onViewCreated를 호출했으면 반드시 onViewDestroyed를 호출해야 합니다. 일반 액티비티는 사실 onDestroyed가 호출되는 것이 보장되지 않긴 하지만 이런 행동이 필요합니다. 테스트를 시행하고 어떤 작업을 하고 있는지 추적하세요.

백스택 오퍼레이션에는 함수 중심 프로그래밍 사용하기

필요하지 않은 경우 이런 최신 기법을 사용하지 않아도 되지만, 구현 세부 사항을 보면 화면으로 이동할 때 새 화면을 스택에 넣는 함수가 있습니다. 이전 화면으로 돌아갈 때도 스택에서 꺼내는 함수가 있죠. 함수 중심 프로그래밍을 사용하면 구현을 훨씬 더 멋지게 할 수 있습니다.

모든 것을 동기로 유지하되, 내비게이팅할 때 포스트하기

제가 이미 시도해봤는데, 여기에도 돌아가는 편법이 없습니다. 특히 다이얼로그를 사용하는 중에 버튼을 눌러서 다른 화면으로 넘어가는 경우, 뷰가 움직이기 시작해야 하기 때문에 포스트해야 합니다. 이런 경우 시스템이 팝업을 없애려고 하기 때문에 크래시가 발생할 수도 있습니다.

뷰가 측정되기를 기다릴 때는 ViewTreeObserver 사용하기

이 전역적인 작업은 좀 불편하긴 하지만 애니메이션을 시작할 때 필요합니다. 나중에 ViewTreeObserver를 제거하는 것만 기억한다면 문제가 발생하지 않을 겁니다.

onDestroy는 호출이 보장되지 않음을 기억하기

액티비티에 WeakReference를 사용하는 것이 해결책이 될 수 있습니다. 뒷정리를 위해 onDestroy에 의지해서는 안됩니다.

향후 나아갈 길 (22:56)

이런 개발 방식이 많은 문제를 일으킬거라고 생각할지도 모르겠습니다. 사실 완벽하지는 않겠죠. 예를 들어 앱이 죽었을 경우 백스택을 자동으로 복원할 수 있는 방법이 없고, 여러 화면에 대한 지원도 현재 기본 지원되지 않습니다. 머티리얼 애니메이션도 아직 지원하지 않지만, 애니메이션 하나가 작성되긴 했습니다.

하지만 저희는 곧 오픈 소스를 공개할 예정입니다. 라이브러리로 사용할 필요 없이 포크해서 커스터마이징하거나 직접 기능을 추가해서 사용할 수 있습니다.

저희의 실제 앱에서는 저희가 오픈 소스로 공개할 최초 버전에는 아마 포함되지 않을 많은 기능들이 사용되고 있습니다. Rx 화면처럼 RxJava 옵져버블을 자동으로 등록 해제하거나 탭을 사용하는 경우 자동으로 탭 레이아웃을 사용하는 탭 화면 등입니다.

다음: Android Architecture #6: 프래그먼트: 안드로이드의 모든 문제의 해결책이자 원인

General link arrow white

컨텐츠에 대하여

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

Fabien Devos

Fabien Devos는 안드로이드 1.0 버전이 배포되기 전부터 안드로이드 앱을 만들어 온 개발자입니다. 파리에 거주하며, Dailymotion, Le Monde, AlloCiné과 같이 유럽에서 유명한 앱을 많이 개발했습니다. 런던으로 이주한 다음에는 Lightbox에 합류했고 Facebook에 합병되면서 Facebook 안드로이드 팀에서 일했습니다. 그 후 Upthere를 거쳐 현재는 Wealthfront의 안드로이드 리드 개발자로 일하고 있습니다. Joaquim Vergès와 함께 60만 번 이상 다운로드된 모바일 코딩 게임, Hacked를 개발하기도 했습니다.

4 design patterns for a RESTless mobile integration »

close