티스토리 뷰


MVP이 뭐에요???

우리가 아는 Most Valuable Player?? 는 아니고 Model View Presenter 의 약자입니다. 근데 일단 정의 보다 중요한게 안드로이드를 개발하다 보면 mvc 패턴, mvp 패턴, mvvm 패턴, 등등 이상한 말들을 많이 들어봤을 겁니다. 여기서 우린 패턴이란 말에 주목할 필요가 있습니다. 패턴은 행위 혹은 사건들이 일정하게 반복되는 것을 의미합니다. 

-패턴의 필요성

이 패턴이란 것이 왜 모두가 중요하다 여기면서 지향점으로 삼고 왜 코딩을 패턴이란것에 맞춰서 진행할까요??


패턴은 일종의 약속의 역할을 합니다. 사람은 모두 개개인이 다르기 때문에 코딩또한 스타일 부터 프로젝트 구조까지 전부 제각각입니다. 그렇게 되면 협업을 할때 서로 같은 언어를 쓰더라도 상대방이 어떻게 구조를 만들었고 어떤 스타일로 코딩하는지 파악하는데 엄청난 시간이 소비될것 입니다. 또한 협업이 아니더라도 개인 프로젝트의 경우에 이전에 했던 코드가 어떤일을 하는지 한달정도가 지나고 다시 몇가지 수정하려고 볼때 처음부터 다시 파악하곤하는 모습을 심심치 않게 볼 수 있을겁니다. 이러한 시간적 소모를 줄이기 위해 코드를 패턴화 즉 프로젝트의 구조를 패턴화 시킴으로써 추후에도 다시봐도 바로파악 가능하게 남들이 봐도 한번에 잘 읽히도록 하는게 중요하기 때문에 우린 디자인패턴을 중요시 여깁니다.


-역시나 서두가 길다 

그래서 우린 패턴 공부를 시작해보는데 그 중에서 필자가 가장 사용 많이하는 MVP패턴에 대해서 알아보려고 합니다. 다른 포스팅들글을 읽으면 MVC,MVP,MVVM 패턴의 장단점을 잘 비교 해놨습니다. 이셋중에서 어떤걸 써야되요? 라고 한다면 회사에서 쓰는 스타일 혹은 본인 스타일에 맞는걸 쓰세요 라고 말하고 싶습니다. 하지만  MVP 는  MVC 모델을 개선한 스타일이기 때문에  MVC를 배우려 한다면 MVP 패턴을 공부해보길 바랍니다. MVVM은 데이터 바인딩을 마스터하고 보길바랍니다.


MVP란 Model View Presenter 라고 했다. Model과 View는 대충 직감적으로 뭘할지 1퍼센트라도 예상이 가능하지만 Presenter 얘는 1도 감이안올것입니다. 아래 그림을 봐보도록 해보자

(사진출저 : 직접제작)

안드로이드에서 Model 은 데이터를 다루거나 어떤 실질적 일을 하는 곳이라고 생각하면 된다. View는 우리가 알고 있는 Activity,Fragment, 위젯 등을 말한다. 그리고 이놈의 Presenter 는 한국말로 하면 증여자이다. 와 한국말도 어렵다. 하지만 풀이를 해보겠다. 증여자라고 함은 말그대로 주는 사람이다. 재산 증여할때 증여자 생각해보면 감이 빨리올 것이다. 위의 그림을 보면 View 에게 모델을 주거나 Model에게 요청을 주는 존재다. 그럼 모델과 뷰가 직접 주고받고 하면 안돼나요? 왜 꼭 프리젠터가 필요한가요? 라고 의문을 가질 것이다. MVP 패턴은 Model과 View를 완전히 분리 시키는데 초점을 맞춘 패턴이다. Model과 View 가 직접 주고 받는다면 뷰가 모델의 역활을 거의 흡수하게 된다.  아래 예시를 한번 보자

public class MainActivity extends AppCompatActivity implements LocationListener, View.OnClickListener,TaskContract.View {
LocationManager locationManager;
double latitude;
double longitude;
TextView weatherTxt;
Button button;
TaskContract.Presenter mPresenter;
TaskPresenter mTaskPresenter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

initView();
locationManager = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE);
mTaskPresenter = new TaskPresenter(this);
}

private void initView() {
//뷰세팅
weatherTxt = (TextView) findViewById(R.id.weather);
button = (Button) findViewById(R.id.button);
button.setOnClickListener(this);
}

@Override
protected void onResume() {
super.onResume();
mPresenter.start();
}

@Override
public void onLocationChanged(Location location) {
/*현재 위치에서 위도경도 값을 받아온뒤 우리는 지속해서 위도 경도를 읽어올것이 아니니
날씨 api에 위도경도 값을 넘겨주고 위치 정보 모니터링을 제거한다.*/
latitude = location.getLatitude();
longitude = location.getLongitude();
//날씨 가져오기 통신'
mPresenter.getWeather(latitude,longitude);
//위치정보 모니터링 제거
locationManager.removeUpdates(MainActivity.this);
}


@Override
public void onClick(View v) {
switch (v.getId()) {
//버튼 클릭시 현재위치의 날씨를 가져온다
case R.id.button:
if (locationManager != null) {
requestLocation();
}

break;
}
}

private void requestLocation() {
//사용자로 부터 위치정보 권한체크
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 500, 1, this);
locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 500, 1, this);
}



@Override
public void showWeather(String weather) {
weatherTxt.setText(weather);
}

@Override
public void setPresenter(TaskContract.Presenter presenter) {
mPresenter = presenter;
}
}

위의 코드는 MVP 패턴의 View의 해당하는 액티비티 클래스의 코드이다. 코드를 보면 알겠지만 미로처럼 코드를 꼬아놓은것이 아닌 기능을 전부 분리시킨것이다. 기능은 현재 위도경도를 통해 날씨를 가져와서 텍스트를 보여주는것이지만 기능을 완전 분리하고 텍스트를 보여주는것만 정의되어 있다. 이렇게 되면 View에선 정말 View에 관련된일만 Model에선 정말 데이터 관련된 일만 하도록 코딩이 가능해진다. 경이롭다!!!


-빨리 작성해보자!!!!!

MVP 패턴의 Presenter 부터 구현을 해보자. Presenter는 주고받고의 역활을 주로하기 때문에 java interface를 이용한다. 먼저 BaseView와 BasePresenter를 만들어보자.


BasePresenter

public interface BasePresenter {
void start();
}

BaseView

public interface BaseView<T> {
void setPresenter(T presenter);
}

초특급 간단하다.

다음으로 Task를 정의해보자. 즉 프리젠터가 해야할일을 정의 해보자이다. 우리는 위치정보를 받아와서 모델에게 위치정보를 넘겨주고 날씨데이터를 받으면 텍스트로 넘겨주는일을 한다.

public interface TaskContract  {
interface Presenter extends BasePresenter {
void getWeather(double lat, double lon);
}
interface View extends BaseView<Presenter> {
void showWeather(String weather);
}
}

날씨데이터엔 위도경도 정보가 필요하니 인자로 위도경도를 받아주고 날씨정보를 String으로 넘겨준다. 위의 코드는 실제 프리젠터를 구현한것이아닌 프리젠터가 가져야할 기능에 대해서만 정의를 한것이다. 위으 MVP구조에서 보면 화살표를 만든 작업을 한것이라 생각하면 이해가 쉽다. 

다음으로 실제 프리젠터를 구현해 보자.

public class TaskPresenter implements TaskContract.Presenter,WeatherCallback {

WeatherRepository weatherRepository;
TaskContract.View view;

public TaskPresenter(TaskContract.View view) {
this.view = view;
view.setPresenter(this);
}

@Override
public void start() {

}

@Override
public void getWeather(double lat, double lon) {
weatherRepository = new WeatherRepository(this);
weatherRepository.getWeather(lat,lon);
}

@Override
public void callback(String weather) {
view.showWeather(weather);
}
}

위의 코드를 보면 TaskContract 의 Presenter를 implement하여 getWeather를 정의하는데 WeatherRepository라는 모델클래스를 만들고 모델클래스로부터 날씨정보를 요청하는 코드를 작성한다. 이때 callback함수를 만들어서 날씨정보 가져오는 것이 완료되면 callback함수를 통해 날씨데이터를 presenter로 받아오고 그것을 뷰에다가 다시 전달하는 코드가 되겠다.

 

public class WeatherRepository {
WeatherCallback weatherCallback;

public WeatherRepository(WeatherCallback weatherCallback) {
this.weatherCallback = weatherCallback;
}

public void getWeather(double latitude, double longitude) {
Retrofit retrofit = new Retrofit.Builder().addConverterFactory(GsonConverterFactory.create())
.baseUrl(ApiService.BASEURL)
.build();
ApiService apiService = retrofit.create(ApiService.class);
Call<JsonObject> call = apiService.getHourly(ApiService.APIKEY, 1, latitude, longitude);
call.enqueue(new Callback<JsonObject>() {
@Override
public void onResponse(@NonNull Call<JsonObject> call, @NonNull Response<JsonObject> response) {
if (response.isSuccessful()) {
//날씨데이터를 받아옴
JsonObject object = response.body();
if (object != null) {
//데이터가 null 이 아니라면 날씨 데이터를 텍스트뷰로 보여주기
weatherCallback.callback(object.toString());
}

}
}

@Override
public void onFailure(@NonNull Call<JsonObject> call, @NonNull Throwable t) {
}
});
}
}

위의코드는 WeatherRepository 코드이다. 위의 코드 이전포스팅과 이어서 진행되니 실습을 위해선 이전포스팅을 필독하자!!(통신예제/ 개념만 이해하려면 패스해도 좋음) 위의 코드는 Retrofit을 이용하여 서버에서 날씨 정보를 가져오는 코드이다. 실제 데이터를 다루는 코드이다. 날씨데이터를 가져오면 callback으로 데이터를 넘겨준다.

public class MainActivity extends AppCompatActivity implements LocationListener, View.OnClickListener,TaskContract.View {
LocationManager locationManager;
double latitude;
double longitude;
TextView latText, lonText, weatherTxt;
Button button;
TaskContract.Presenter mPresenter;
TaskPresenter mTaskPresenter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

initView();
locationManager = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE);
mTaskPresenter = new TaskPresenter(this);
}

private void initView() {
//뷰세팅
latText = (TextView) findViewById(R.id.latitude);
lonText = (TextView) findViewById(R.id.longitude);
weatherTxt = (TextView) findViewById(R.id.weather);
button = (Button) findViewById(R.id.button);
button.setOnClickListener(this);
}

@Override
protected void onResume() {
super.onResume();
mPresenter.start();
}

@Override
public void onLocationChanged(Location location) {
/*현재 위치에서 위도경도 값을 받아온뒤 우리는 지속해서 위도 경도를 읽어올것이 아니니
날씨 api에 위도경도 값을 넘겨주고 위치 정보 모니터링을 제거한다.*/
latitude = location.getLatitude();
longitude = location.getLongitude();
//위도 경도 텍스트뷰에 보여주기
latText.setText(String.valueOf(latitude));
lonText.setText(String.valueOf(longitude));
//날씨 가져오기 통신'
mPresenter.getWeather(latitude,longitude);
//위치정보 모니터링 제거
locationManager.removeUpdates(MainActivity.this);
}

@Override
public void onStatusChanged(String provider, int status, Bundle extras) {

}

@Override
public void onProviderEnabled(String provider) {

}

@Override
public void onProviderDisabled(String provider) {

}
@Override
public void onClick(View v) {
switch (v.getId()) {
//버튼 클릭시 현재위치의 날씨를 가져온다
case R.id.button:
if (locationManager != null) {
requestLocation();
}

break;
}
}

private void requestLocation() {
//사용자로 부터 위치정보 권한체크
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED
&& ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION}, 0);
} else {
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 500, 1, this);
locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 500, 1, this);

}
}



@Override
public void showWeather(String weather) {
weatherTxt.setText(weather);
}

@Override
public void setPresenter(TaskContract.Presenter presenter) {
mPresenter = presenter;
}
}

위의 코드는 위에서 본 View의 코드의 풀버전이다. 버튼을 클릭하면 위치정보를 가져와서 presenter로 위도경도 정보를 넘겨주고 Presenter는 위도경도 정보를 WeatherRepository에 요청하고 callback함수로 데이터를 받아와 showWeather 로 넘겨주어 날씨정보를 View 에 보여준다.


지금까지 MVP패턴에 관하여 예제를 진행해봤다. 이 포스팅엔 중요부분코드만 작성되어있으니 GITHUB < 여기에 풀코드를 올려 놨다. 


MVP패턴은 참고로 View하나당 Presenter하나이다. Model은 여러 Presenter를 가질 수 있다. 이개념은

(사진출저 : 직접제작)

서버의 개념과도 비슷하다. 안드로이드는 View로 서버가 Presenter로 저장소가 Model이라고 대입해서 이해하면 쉽다. 정확히 일맥상통하는 개념은 아니지만 이해를 위해선 이개념보다 더 좋을 개념이 없다 생각한다.


이상으로 MVP패턴 설명끝!!!!!


ps.


비전공자 안드로이드 질문방을 운영중입니다. 

톡방링크 (링크) 를 통해 들어오시면 못다 설명드린내용들 자세히 설명드릴게요!!! 

이깟 블로그보다 직접만나서 배워보고 싶으시면 말리지 않습니다. 어서오세요 (링크)

마지막으로....제가 만든 앱 (링크) 입니다. 리뷰... 하나가 생명을 살립니다. 감사합니다.


 

댓글
  • 프로필사진 karorok 지금까지 본 mvp중 제가 보기에는 가장 좋았던거 같아요...
    다 이해한건 아니지만 그래도 먼가 살짝 지푸라기라도 잡은 느낌이 있는것 같아요 여러개의 포스팅을 봤지만!
    2017.11.20 23:27 신고
  • 프로필사진 Favicon of http://www.feelteller.com Edge_JH 감사합니다 더좋은 포스팅으로 찾아뵙겠습니다. 덕분에 호랑이 기운이 솟구칩니다! 2017.11.21 16:15 신고
  • 프로필사진 boundarydev 아직은 어렵네요 ㅠㅠ
    포스팅 보고 이해 조금했지만 뭔가 개념들이 미미하게 떠다닙니다
    포스팅 감사합니다!
    2018.02.07 16:45 신고
댓글쓰기 폼