2015년 03월 01일 | Steve.Jung

AndroidAnnotations 과 MVC 패턴

이 포스팅은 총 4부로 이어지며 현재는 3부입니다.

  1. 1부 : Android, MVC, MVVM, MVP
  2. 2부 : Android 와 Annotation
  3. 3부 : AndroidAnnotations 과 MVC
  4. 4부 : AndroidAnnotations 과 테스트

앞선 포스팅 2개를 통해서 Android 에서의 개발 패턴과 Annotation 에 대해 알아보았습니다.

이번에는 Tosslab 의 Jandi App 에서 사용하는 AndroidAnnoations 와 MVC 모델을 적용하는 과정을 보여드리고자 합니다.

다음은 일반적인 형태의 모습을 가진 Android 코드를 MVC 로 변화하는 간단한 예시입니다.

앱 시나리오

앱 목적 : 리눅스 릴리즈 상태 확인하기

EditText 에 리눅스 버전을 입력하면 Github 에 접근 하여 릴리즈 정보가 있는지 확인합니다.

릴리즈 정보가 있으면 release 로 표시 릴리즈 정보가 없으면 not release 로 표시

※ http 통신은 pseudo 코드로 표현하도록 하겠습니다.

기존 코드

Public class MainActivity extends Activity {

	private EditText versionEdtiText;
	private Button checkButton;
	private TextView releaseText;
	
	private Handler handler;

	@Override
	public void onCreate(Bundle saveInstance) {
		super.onCreate(saveInstance);
		setContentView(R.layout.act_main);
		
		versionEdtiText = (EditText) findViewById(R.id.et_version);
		checkButton = (Button) findViewById(R.id.btn_check);
		releaseText = (TextView) findViewById(R.id.tv_release);
		handler = new Handler();
		
		checkButton.setOnClick(new View.OnClick() {
			@Override
			public void onClick(View view) {
				String version = versionEditText.getText().toString;
				
				new Thread(new VersionCheckRunnable(MainActivity.this, version, new Callback(){
				public void onCheckResult(final boolean isRelease) {
					handler.post(new Runnable(){
						public void run() {
							if (isRelease) {
								releaseText.setText("release);
							} else {
								releaseText.setText("not release);
							}
						}
					});
				}
				})).start();
			}
		});
	}
	
	static class VersionCheckRunnable implement Runnable {

		private final Context context;
		private final String version;		
		private final Callback callback;
		
		public VersionCheckRunnable(Context context, String version, Callback callback) {
			this.version = version;
			this.context = context;
			this.callback = callback;
		}
		
		@Override
		public void run() {
			boolean isReleased = getReleaseState(version);
			if (callback != null) {
				callback.onCheckResult(isReleased);
			}
		}
		
		private boolean getReleaseState(String version) {
			// ... 중략
		}
	}
	
	static interface Callback {
		void onCheckResult(boolean isReleased);
	}
}

위의 사례는 조금 극단적인 안드로이드 개발 코드입니다. MVC 조차로도 구현되어 있지 않은 코드 상태입니다.

 
사용자가 버젼을 입력 -> 버튼 누르기

위의 동작을 수행하면

새로운 Thread 를 생성하여 서버와 통신을 시작합니다. 통신이 완료되면 Handler 에게 결과를 전송하여 UI 갱신을 하도록 합니다.

멀티쓰레딩 처리, View 바인딩, UI 처리가 혼합되어 있어 코드에 대한 가독성이 극단적으로 좋지 않은 형태입니다. 만약 이러한 처리가 하나의 Activity 에서 다양하게 존재한다면 유지보수성은 최악이 될 가능성이 농후해집니다.

이를 아래에서 AndroidAnnotations 을 이용하여 MVC 패턴으로 적용해보도록 하겠습니다.

MVC 로 적용된 모습

@EActivity(R.layout.act_main)
public class MainActivity extends Activity {

	@Bean
	MainView MainView;
	
	@Bean
	MainModel mainModel;

	@Click(R.id.btn_check)
	@Background
	void onCheckClick(View view) {
		String versionText = MainView.getVersionText();
		
		boolean isRelease = mainModel.getReleaseState(version);
		
		if (isRelease) {
			MainView.setReleaseText("release");
		} else {
			MainView.setReleaseText("not release");
		}
	}
}
@EBean
public class MainView {

	@ViewById(R.id.et_version)
	EditText versionEditText;
	
	@ViewById(R.id.tv_release)
	TextView releaseText;

	public String getVersionText() {
		return versionEditText.getText().toString();
	}
	
	@UiThread
	public void setReleaseText(String version) {
		releaseText.setText(version);
	}
}
@EBean
public class MainModel {

	public boolean getReleaseState(String version) {
		// ...중략...
		// 기존 VersionCheckRunnable 의 코드를 그대로 가져온다.
	}

}

Model 은 서버와 통신을 수행하고 ViewController 는 View 에 직접 접근하도록 정의하였습니다. Activity 는 Controller 의 역할을 위해 Model 과 ViewController 에 대한 정보와 View Event 만을 정의하였습니다.

여기서 눈여겨볼 점은 다음 코드입니다.

public class MainActivity extends Activity {

	@Click(R.id.btn_check)
	@Background
	void onCheckClick(View view) {...}

}
public class MainView {
	@UiThread
	public void setReleaseText(String version) {...}
}
  1. 버튼의 Click 이벤트정의를 @Click({Resource ID}) 만으로 정의하였다는 점
  2. MultiThread 처리를 @Background 로 정의한 점
  3. MainThread 처리 @UiThread 로 정의한 점

AndroidAnnotations 으로 정의된 클래스를 APT 로 컴파일한 후 클래스를 보면

  1. View.setOnClickListener 가 직접 정의된 모습
  2. MultiThread 처리는 AndroidAnnotations 내부에서 정의된 ThreadPool 에서 실행시키는 모습
public class MainActivity_ extends MainActivity {
	// ...중략...
	
	public void init_() {
		View hasView = findViewById(R.id.btn_check);
		if (hasView != null) {
			hasView.setOnClickListener(new View.OnClickListener() {
				@Override
				public void onClick(final View view) {
					BackgroundExecutor..execute(new BackgroundExecutor.Task("", 0, "") {

		            @Override
		            public void execute() {
		                try {
		                    MainActivity_. onCheckClick(view);
		                } catch (Throwable e) {
		                    Thread.getDefaultUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e);
		                }
		            }
				}
			});
		}
	}
}
  1. MainThread 처리는 Handler.post(new Runnable(){…}) 을 이용한 모습
public class MainView_ extends MainView {

	Handler handler_;

	public MainView_() {
		handler_ = new Handler();
	}
	
	@Override
	public void setReleaseText(String version) {
		handler_post(new Runnable() {
			@Override
			public void run() {
				MainView.super.setReleaseText(version);
			}
		});
	}

위와 같은 모습을 확인할 수 있습니다.

결과적으로 우리가 작성하는 코드들은 최대한 적은 Depth 로 구현하고 실질적인 동작은 AndroidAnnotations 을 통해서 정의됨을 볼 수 있습니다.

이로써 MVC 단점인 View 와 Event 에 대한 바인딩이 콜백에 의해 정의되는 것을 최소화 하면서 Model-View 가 분리되고 Activity 가 Controller 의 역할을 수행하는 것을 알 수 있습니다.

MVC 를 구현하기 위한 노하우

  • MVC 의 Activity 는 View? Controller!

Activity 의 역할이 가장 중요하다 View 의 성격도 같이 가지고 있는 Activity 에서 view 처리는 모두 View 에서 처리하도록 합니다. 그리고 Activity 는 View 나 외부에서 들어오는 이벤트 등을 받아서 Model - View 사이에서 로직을 제어하는 역할만 합니다.

  • Callback 코드의 최소화

안드로이드 코드들은 View 에 대한 이벤트 정의를 Callback 형태로 정의합니다. 하지만 이러한 코드 형태는 Background 와 Main Thread 를 오가는 환경이 생긴다면 Callback Hell 이라고 부르는 지경에 이르게 됩니다.

하지만 예제에서는 View 이벤트는 ViewController 에서 직접적으로 받을 수 있도록 하되 View 에 접근하는 코드를 최소화 하기 위해서 Event 에 대해서는 Annotation 을 통해 코드 가독성이 떨어지는 Callback 을 최소화 하는 구조를 변경하였습니다.

간혹 Dialog 와 같이 직접적인 접근이 어려운 곳은 EventBus 와 같은 Observer 를 통해서 처리할 수 있도록 하여 가능한 구조의 일관성을 가지고자 합니다.

  • Background 로직이 필요하는 경우

AndroidAnnotations 은 @Background 가 선언된 메소드는 Background 에서 동작하도록 제어합니다. 별도로 Thread 를 선언할 필요가 없으며 다시 Ui Thread 에 접근하고자 할 때는 @UiThread 를 통해 접근할 수 있습니다.

또한 @SupposedBackground 와 @SupposedUiThread 를 통해서 현재 메소드가 원하는 Thread 에서 접근하는 것인지 Assertion 을 지원합니다. 위의 Annotation 은 Runtime 동작하여 Runtime 오류 가능성이 있습니다.

@Background -> @UiThread 접근시 유의점 @UiThread 가 선언된 코드는 내부 동작이 Handler.post(…) 를 통해서 실행됩니다. 따라서 @UiThread 를 연속으로 실행한다고 해서 동작의 순서가 보장되지 않습니다. 가급적 연관된 동작은 하나의 @UiThread 에 정의를 해주는 것이 좋습니다.

  • DI 기능 적극 활용

Reflection 에 의한 View DI 는 필연적으로 Runtime 시 성능에 영향이 가는 동작방식입니다.

하지만 Android-Annotation 의 DI 는 Annotation Procession Tool (APT) 를 이용하여 동작하기 때문에 생성된 코드에 “_” 가 붙는 단점이 있긴 하나 DI 과정에서 성능상 영향을 거의 주지 않습니다.

※ View DI 용도만을 위함이라면 Dagger 나 ButterKnife 도 좋은 해결책입니다. 두 라이브러리 모두 APT 를 사용하여 View DI를 합니다.

결론

처음 AndroidAnnotations 을 접했을 때는 생성된 코드에 “_” 를 붙여야만 접근할 수 있는 좋지 않는 형태를 가지고 있었습니다.

하지만 이러한 단점을 제외한다면 구조적으로 MVP 모델에 매우 적합한 모습을 유지할 수 있는 코드를 만들어주는 장점을 가졌습니다.

경험적으로 MVC, MVVM, MVP 를 모두 구현하고자 했을 때 AndroidAnnotations 은 MVC 가 가지고 있는 Controller - View 간의 이벤트 Callback 처리에 대한 단점 또한 유연하게 대처할 수 있도록 해주었습니다. (@Click 과 같은 이벤트 Annotation 으로…)

현재 Jandi 팀은 AndroidAnnotations 과 MVC 모델을 적극적으로 도입하여 사용하고 있으며 UnitTest 작성에도 View 와 Model 이 분리하여 작성할 수 있었습니다.