태그 보관물: Android

Android, MVC, MVVM, MVP

원본링크(Android 와 개발 패턴 1부)

코드를 작성해 가는데 있어서 많은 개발자들은 일관성을 유지하고 유지보수성을 높일 수 있는 많은 개발 모델들을 생각해 왔습니다. 특히나 협업하는 과정에서 로직과 View 를 다루는 코드가 뒤섞이게 되면 작성자뿐만 아니라 동료들조차도 유지보수 하기 힘들어지는 모습을 쉽게 볼 수 있습니다.

이러한 코드 작성때문에 팀단위로, 프로젝트 단위로, 때로는 회사 단위로 여러가지 정책을 가지고 코드와 개발 모델들을 일관되게 하려고 많은 노력을 합니다. 이러한 노력에 개발자들 사이에서 많이 통용되는 개발 패턴들이 나오게 되었습니다.

대표적으로 MVC 모델이라고 할 수 있습니다. 필자가 과거에 했던 웹 프로젝트 중 SpringMVC 프레임워크는 외부로부터의 요청을 처리하는 Controller, 실질적인 비지니스 로직을 수행하는 Model, 그리고 화면 처리를 담당하는 View 를 구분할 수 있도록 지원되어 많은 개발자들에게 사랑받고 있는 프레임워크 중 하나입니다.

차츰 View 자체에서 처리해야하는 로직이 복잡해짐에 따라 MVVM과 MVP 패턴이 나오면서 View 내에서도 Logic과 Presenter 를 구분하려는 노력이 끊임없이 나왔습니다.

이외에도 많은 개발 패턴들이 있지만 가장 많이 통용되는 이야기를 하고자 합니다.

초창기 안드로이드 개발 코드의 모습

안드로이드에서도 초창기 부터 이러한 노력이 끊임없이 나왔습니다. MVC 와 같은 개발 패턴에 익숙해져 있던 많은 개발자들은 안드로이드에도 이와 같은 모습을 적용하려고 노력하였습니다.

다음 코드는 일반적으로 처음 안드로이드를 접하는 사람들이 쓰는 코드들입니다.

[code language=”java”]
public class MainAcivity extends Activity
{

@Override
public void onCreate( Bundle saveInstance )
{
super.onCreate( saveInstance );
setContent( R.layout.main );

TextView textView = ( TextView ) findViewById( R.id.btn_confirm );
textView.setText( "Non-Clicked" );

findViewById( R.id.btn_confirm ).setOnClickListener( new View.OnClickListener()
{

@Override
public void onClick( View view )
{
TextView textView = ( TextView ) findViewById( R.id.btn_confirm );
textView.setText( getClickedText() );
}
} );
}

String getClickedText()
{
return "Clicked!!!";
}

}
[/code]

Activity 내에 이벤트를 핸들링하는 처리나 뷰에 접근하는 코드들이 모두 있습니다.

 

이러한 코드들의 모습은 서버기반 동작시엔 하나의 Activity 내에 네트워크 처리를 위한 쓰레드 처리까지 하게되는 등 코드가 커지면 커질수록 가독성도 떨어지며 유지보수가 힘들어지는 코드로 가기 쉬워집니다.

그래서 기존의 웹에서처럼 좀 더 기능별로 분할하여 코드들을 간결하고 유지보수가 쉬워지도록 하기 위한 방법들이 많이 도입되기 시작하였습니다.

나은 코드들을 위한 패턴들

하지만 화면이 점점 복잡해지게 되고 점점 View 에 의해 제어되는 로직들이 많아짐에 따라 이를 분리하고하는 노력들이 나왔습니다.

다음은 웹 서비스 개발자들에게 가장 쉽게 사용되었던 MVC 패턴을 적용한 코드입니다.

[code language=”java”]
public class MainAcivity extends Activity
{
private MainModel mainModel;

@Override
public void onCreate( Bundle saveInstance )
{
super.onCreate( saveInstance );
setContent( R.layout.main );

mainModel = new MainModel();

TextView textView = ( TextView ) findViewById( R.id.btn_confirm );
textView.setText( "Non-Clicked" );

findViewById( R.id.btn_confirm ).setOnClickListener( new View.OnClickListener()
{

@Override
public void onClick( View view )
{
String text = mainModel.getClickedText();
TextView textView = ( TextView ) findViewById( R.id.btn_confirm );
textView.setText( mainModel.getClickedText() );
}
} );
}
}
[/code]

[code language=”java”]
public class MainModel
{
public String getClickedText()
{
return "Clicked!!!";
}
}
[/code]

View 에 상관없는 로직을 MainModel 로 분리를 하였기 때문에 Activity 는 View 와 Click Event 를 처리하는 모습으로 변화되었습니다.

하지만 Click Event 와 View 에 대한 처리가 함께 있는 것을 유심히 생각해볼 필요가 있습니다. MVC 모델에서 Controller 는 View 에 대한 처리를 직접 하는 것이 아니라 View 에 대한 정보만을 View 에 전달함으로써 화면을 그리는 View 와 동작을 처리하는 로직을 분리하고자 하는데서 시작되었습니다.

헌데 Controller 의 역할을 수행하는 Activity 에서 View 에 대한 직접적인 조작을 수행하는 것은 MVC 모델에 어긋나는 모습을 보여주게 됩니다.

이는 Android 에서 Activity(Fragment) 가 View 와 Controller 두가지의 특성을 모두 가지고 있기 때문에 View 나 Controller 를 한쪽으로 빼게 될 경우 View 에 대한 바인딩이나 처리에서 중복 코드나 일관성을 잃어버리는 코드를 작성할 수 있기 때문입니다. 이를 개선하기 위해서 MVVM 이란 패턴을 적용해봤습니다.

[code language=”java”]
public class MainAcivity extends Activity
{

private MainViewModel mainViewModel;

@Override
public void onCreate( Bundle saveInstance )
{
super.onCreate( saveInstance );
setContent( R.layout.main );

mainViewModel = new MainViewModel( MainActivity.this );

}

}
[/code]

[code language=”java”]
public class MainModel
{

public String getClickedText()
{
return "Clicked!!!";
}

}
[/code]

[code language=”java”]
public class MainViewModel
{

private Activity activity;
private MainModel mainModel;
private TextView textView;

public MainViewModel( Activity activity )
{
this.activity = activity;
this.mainModel = new MainModel();
initView( activity );
}

private void initView( Activity activity )
{

textView = ( TextView ) activity.findViewById( R.id.btn_confirm );
textView.setText( "Non-Clicked" );

activity.findViewById( R.id.btn_confirm ).setOnClickListener( new View.OnClickListener()
{

@Override
public void onClick( View view )
{
String text = mainModel.getClickedText();
textView.setText( text );
}
} );
}

}
[/code]

ViewModel 로 View 에 대한 처리가 분리되었음을 볼 수 있습니다.

하지만 안드로이드에서 MVVM이 가지는 문제점은 View 에 대한 처리가 복잡해질수록 ViewModel 에 거대해지게 되고 상대적으로 Activity 는 아무런 역할도 하지 않는 형태의 클래스로 변모하게 됩니다.

Controller 의 성격을 지닌 Activity 가 실질적으로 아무런 역할을 하지 못하고 ViewModel 에 치중된 모습을 보여줌으로써 다른 형태의 Activity 클래스를 구현한 꼴이 되어버리는 것입니다.

MainViewModel 에 있는 로직을 다시 Activity 로 롤백한다하면 다시 MVC 가 가지고 있는 문제점을 가지게 되는 아이러니한 모습을 가지게 됩니다.

다음은 이러한 View – Model – Controller 의 모습을 명확히 구분하고자 나온 MVP 모델을 보도록 하겠습니다.

[code language=”java”]
public interface MainPresenter
{

void setView( MainPresenter.View view );

void onConfirm();

public interface View
{
void setConfirmText( String text );
}

}
[/code]

[code language=”java”]
public class MainAcivity extends Activity implements MainPresenter.View
{

private MainPresenter mainPresenter;

private Button confirmButton;

@Override
public void onCreate( Bundle saveInstance )
{
super.onCreate( saveInstance );
setContent( R.layout.main );

mainPresenter = new MainPresenterImpl( MainActivity.this );
mainPresenter.setView( this );

confirmButton = ( Button ) findViewById( R.id.btn_confirm );
confirmButton.setOnClickListener( new View.OnClick()
{
@Override
public void onClick( View view )
{
mainPresenter.onConfirm();
}
} );
}

@Override
public void setButtonText( String text )
{
confirmButton.setText( text );
}
}
[/code]

[code language=”java”]
public class MainModel
{

public String getClickedText()
{
return "Clicked!!!";
}

}
[/code]

[code language=”java”]
public class MainPresenterImpl implements MainPresenter
{

private Activity activity;
private MainPresenter.View view;

public MainPresenterImpl( Activity activity )
{
this.activity = activity;
this.mainModel = new MainModel();
}

@Override
public void setView( MainPresenter.View view )
{
this.view = view;
}

@Override
public void onConfirm()
{
if( view != null )
{
view.setConfirmText( mainModel.getClickedText() );
}
}

}
[/code]

MVP 모델의 구분은 다음과 같다.

  1. View 는 실제 view 에 대한 직접적인 접근을 담당한다.
  2. view 에서 발생하는 이벤트는 직접 핸들링하나 Presenter 에 위임하도록 한다.
  3. Presenter 는 실질적인 기능을 제어하는 곳으로써 ViewController 로써 이해하면 쉽다.
  4. Model 은 비지니스 로직을 실질적으로 수행한다.

Presenter : View 는 1:1 로 매칭하며 View Presenter 가 주요 기능을 관장하되 실제 view 에서 발생하는 이벤트는 Presenter (이벤트 : View -> Presenter) 로 전달하여 처리하도록 하고 다시 처리된 결과는 Presenter 가 View 에 전달하도록 하여 처리한다. (처리 결과 표현 : Presenter -> View)

정리

MVC
외부의 모든 이벤트를 Controller(Activity) 에서 처리하되 View 에 관여는 하지 않는 것이 원칙입니다. 하지만 Activity 의 특성상 View 관여하지 않을 수 없습니다. 결국 Android 에서는 MVC 의 구조를 계속적으로 유지하기에는 무리가 있습니다.

MVVM
ViewModel 이 뷰와의 상호작용 및 제어에 집중합니다. ViewModel 에서 직접 뷰의 이벤트를 처리하기 때문에 Controller 의 역할도 함께 수행하게 되어 점점 코드가 집중 되는 구조가 될 수 있습니다. 또한 초기화와 외부 이벤트(뷰에 의한 것이 아닌 이벤트)를 제외 하고는 Activity 의 역할이 모호해지게 됩니다.

MVP
View 에 대한 직접적인 접근이 요구되는 Android 의 Activity 는 직접적인 view 접근은 Activity 가 하도록 하고 이에 대한 제어는 Presenter 가 하도록 하고 있다.

어느 것이 낫다라고 말하기에는 어려운 단계입니다. 하지만 많은 개발자들이 안드로이드에서는 MVC 자체의 모습보다는 MVVM 이나 MVP 가 가장 적합하다는 말을 많이 하고 있습니다. 이는 Activity 가 View 를 포함한 클래스이기 때문에 나타나는 현상이라고 볼 수 있습니다.

참고 블로그

MVC, MVP, MVVM 의 이해 – 이항희

MVC, MVP AND MVVM – tomyrhymond

This Handler class should be static or leaks might occur

원글 : http://www.androiddesignpatterns.com/2013/01/inner-class-handler-memory-leak.html

[code lang=”js”]
public class SampleActivity extends Activity
{
private final Handler mLeakyHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// …
}
}
}
[/code]

In Android, Handler classes should be static or leaks might occur.

이것이 나오는 원인은

1. Android Application이 실행될 때, Framework에서는 Application의 메인 쓰레드를 위한 Loop Object를 생성 한다. Looper는 단순한 메시지 큐를 생성하여 Message들을 처리하고, Application의 Lifecycle과 동일한 Lifecycle을 갖는다.
2. 메인 쓰레드에서 생성된 Handler는 Looper의 메시지 큐에 속하게 된다. 메시지 큐로 보내진 Message들은 Handler에 대한 reference를 갖고 있어야함, Looper가 해당 메시지를 처리 할수 있다. Looper가 메시지를 처리할 때 Handler#handleMessage(Message)를 호출해야 하기 때문.
3. Java에서는 non-static inner class와 anonymous class는 outer class에 대한 implicit reference를 갖고, static inner class의 경우에는 갖지 않는다. 

Handler와 Looper, Message Queue의 관게을 해결하기 위해 위 문제를 해결하기 위해서는 Handler를 static inner class로 정의하거나, 새로운 파일에 subclass해야 한다. static inner class는 outer class에 대한 implicit reference를 갖기 않기 때문에 Activity Context가 Garbage Collect되는 것을 방해하지 않을 것이다. 만약 Handler 내부에서 Activity의 method를 호출해야 할 경우 Activity의 WeakReference를 갖도록 한다.

[code lang=”js”]
public class SampleActivity extends Activity
{

/**
* Instances of static inner classes do not hold an implicit
* reference to their outer class.
*/
private static class MyHandler extends Handler
{
private final WeakReference<SampleActivity> mActivity;

public MyHandler( SampleActivity activity )
{
mActivity = new WeakReference<SampleActivity>( activity );
}

@Override
public void handleMessage( Message msg )
{
SampleActivity activity = mActivity.get();
if( activity != null )
{
// …
}
}
}

private final MyHandler mHandler = new MyHandler( this );

/**
* Instances of anonymous classes do not hold an implicit
* reference to their outer class when they are "static".
*/
private static final Runnable sRunnable = new Runnable()
{
@Override
public void run()
{
}
};

@Override
protected void onCreate( Bundle savedInstanceState )
{
super.onCreate( savedInstanceState );

// Post a message and delay its execution for 10 minutes.
mHandler.postDelayed( sRunnable, 600000 );

// Go back to the previous Activity.
finish();
}
}
[/code]