한동안 운영하고 있는 어플리케이션 때문에 바빠서 포스팅을 하지 못했다.
그때 적었던 기억을 떠올리며 이어서 작성해보려고한다.
이전 포스팅에서 MVP 패턴에 대해 개요를 적었는데 오늘은 MVP 에 대해 조금 더 알아보자.
이전에 있던 포스팅에서는 자바 코드로 설명했지만 해당 코드의 MVP 패턴에 마음에 안드는 부분들이있어서
그 뒤에 코틀린으로 제작한 프로젝트로 예시를 들겠다.
언어만 다를뿐 패턴자체는 차이가 없으니 MVP 패턴에 대해 읽어보기에는 전혀 문제가 없을 것이다.
아래에 설명될 코드는 회원가입에 대한 코드다.
먼저 로그인하는 화면이 있다고 하자.
class LoginActivity : AppCompatActivity(), MemberContract.View {
var nickname : String?=null
var passWord : String?=null
lateinit var view2 : View
lateinit var imm : InputMethodManager
private lateinit var mpresenter: MemberPresenter // 회원가입과 로그인을 담당하는 프레젠터
lateinit var prefs2 : SharedPreferences
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
prefs2 = getSharedPreferences("LoginNickname", Context.MODE_PRIVATE)
mpresenter = MemberPresenter()
mpresenter.setView(this)// View 와 프레젠터를 연결하는 과정
imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
btn_loginComplete.setOnClickListener {view ->
view2= view
nickname = editId.text.toString()
passWord = editPassWord.text.toString()
if(nickname == "" || passWord == ""){
imm.hideSoftInputFromWindow( loginLin.windowToken ,0)
Snackbar.make(view ,"입력 하지않은 값이 있어요." ,Snackbar.LENGTH_SHORT).setBackgroundTint(resources.getColor(R.color.colorPrimary)).show()
}else{
mpresenter.memberLogin(nickname!! , passWord!!)//공백없이 입력되면 프레젠터가 실행된다.
prefs2.edit().putString("LoginNickname",nickname).apply() // 프리팹에 아이디 넣는장면
// 사실 윗줄의 코드는 여기서처리하면 오류가 발생했을때도 SharedPreferences에 값이 저장되기때문에
//로그인을 성공했다는 콜백이 오면 그때 처리해주는게 맞다.
}
}
}
override fun goMain2() {// 로그인이 성공했을때 호출되는 메소드
val LoginIntent = Intent(this, MainActivity2::class.java)
startActivity(LoginIntent)
finish()
}
override fun waringShackBar() { // 로그인에 실패했을때 호출되는 메소드
imm.hideSoftInputFromWindow( loginLin.windowToken ,0)
Snackbar.make(view2 ,"로그인에 실패했습니다." ,Snackbar.LENGTH_SHORT).setBackgroundTint(resources.getColor(R.color.colorPrimary)).show()
}
override fun waringShackBar2(string: String) {
imm.hideSoftInputFromWindow( loginLin.windowToken ,0)
Snackbar.make(view2 ,string ,Snackbar.LENGTH_SHORT).setBackgroundTint(resources.getColor(R.color.colorPrimary)).show()
}
}
위 코드는 로그인 액티비티의 코드 전문이다. MVP 에서 V 즉 View 를 담당한다. 자세한 내용은 아래 프레젠터를 설명하면서 덧붙이도록 하겠다.
먼저 코드부터 보자.
View ,Presenter 그리고 Model이 서버와 통신을 해서 성공하고 실패했음을 Presenter를 통해 View로 성공과 실패 결과를 알려줄 InfoDataSource 가 MemberContract 라는 interface 안에 구현되어있다.
interface MemberContract {
interface View {
fun goMain2()
fun waringShackBar()
fun waringShackBar2(string : String)
}
interface Presenter{
fun setView(view : View)
fun memberJoin(userNickname:String , passWord :String)
fun memberLogin(userNickname:String , passWord :String)
fun getmemberLikeCount(userNickname: String)
}
interface InfoDataSource {
interface LoadInfoCallback {
fun onInfoLoaded(ss : String)
fun onDataNotAvailable(ff : String)
}
}
}
아래는 Presenter 코드다.
위에서 만들어준 Interface 인 MemberContract 의 Presenter 와 InfoDatsSource를 상속받고 있다. 액티비티 코드를 자세히 봤다면 액티비티에서는 View를 상속받고 있다는것을 봤을것이다.
class MemberPresenter : MemberContract.Presenter , MemberContract.InfoDataSource{
private var MemberView : MemberContract.View? = null
private var MemberModel : MemberModel? =null
override fun setView(view : MemberContract.View){ // view랑 붙이는 작업
MemberView = view
MemberModel = MemberModel(this)
}
override fun memberJoin( userNickname: String, passWord: String) {
Log.v("wgeg2", userNickname + passWord)
MemberModel?.MemberJoin(userNickname , passWord ,object : MemberContract.InfoDataSource.LoadInfoCallback{
override fun onInfoLoaded(a : String) {
if(a.equals("성공")){
MemberView?.goMain2()
}else if(a.equals("정보없음")){
MemberView?.waringShackBar()
}
}
override fun onDataNotAvailable(ff : String) {
Log.v("ffffail" , "실패패ㅐ패패") // 콜백 방식으로 잘 작동하는거 확인
}
}) // 모델에 가입 요청 보냄
}
override fun memberLogin(userNickname: String, passWord: String ) {
MemberModel?.MemberLogin(userNickname , passWord ,object : MemberContract.InfoDataSource.LoadInfoCallback{
override fun onInfoLoaded(ss: String) {
if(ss.equals("성공")){
MemberView?.goMain2()
}else if(ss.equals("정보없음")){
MemberView?.waringShackBar()
}
}
override fun onDataNotAvailable(ff : String) {
if(ff.equals("실패")) {
MemberView?.waringShackBar2("네트워크 통신에 실패했습니다.")
}
}
}) // 로그인 요청 보냄
}
override fun getmemberLikeCount(userNickname: String) {
MemberModel?.getmemberLikeCount(userNickname , object : MemberContract.InfoDataSource.LoadInfoCallback{
override fun onInfoLoaded(ss: String) {
MemberView?.waringShackBar2(ss)
}
override fun onDataNotAvailable(ff: String) {
}
})
}
}
액티비티쪽 코드를 보면
mpresenter.memberLogin(nickname!! , passWord!!) 이런 코드가 있다.
아이디와 패스워드를 채워서 눌렀을때 프레젠터쪽 코드를 실행시키는데
이때 프레젠터에 있는 memberLogin이 실행된다. 액티비티에서 넘어온 userNickname과 password 를 받아서
Model로 넘기고 해당 결과를 LoadInfoCallback으로 받아온다.
그리고 넘어온 결과가 "성공"이라면 MemberView에 있는 goMain2가 실행되는 순으로 진행된다.
실패하면 waringSnackBar 가 실행된다.
goMain2 , waringSnackBar 는 액티비티에서 override로 만들어져 있는 메소드라는걸 알 수 있다.
여기까지보면 프레젠터는 전 포스팅에 있었던 그림처럼 View와 Model 사이에서 중계자 역할을 한다.
액티비티에서 유저의 동작을 정보로 받고 받은 정보를 Model로 보내고
Model에서 실행한 결과를 다시 받아와서 View로 보내준다. 중간 다리 역할을 한다.
그럼 이쯤에서 궁금한게 생긴다.
왜 Model 은 Interface를 만들때 넣지 않았지?
그 이유는 MVP 패턴 자체가 view와 model의 의존성을 해결하기 위해 등장한 패턴이기 때문이다.
예를 들면 액티비티에서 버튼을 눌러서 회원가입을 하는데 회원가입 로직부터 서버와 통신까지 액티비티에서 다 처리하는 상황이다.
이런 코드구성은 작동은 잘할수도 있겠지만 코드를 수정하거나 화면을 바꿔야할때 유지보수적인 측면에서 아주 비효율적이다. 그래서 이렇게 의존성을 낮춰서 model은 model 대로 View는 View대로 따로 존재해도 문제없도록 코드를 작성하는 것이다.
그러나 MVP 패턴도 만능은 아니다. VIew와 Presenter는 여전히 의존적인 관계이며 어플리케이션이 복잡해지면 이 또한 불편한 상황이 생긴다.
그럼 마지막으로 Model 쪽 코드를 보겠다.
class MemberModel (presenter : MemberContract.Presenter) : MemberContract.InfoDataSource {
private var retrofit : Retrofit = NetRetrofit.getInstance()
private var mservice : MemberRetrofitService = retrofit.create(MemberRetrofitService::class.java)
fun MemberJoin(userNickname : String , passWord : String , callback: MemberContract.InfoDataSource.LoadInfoCallback){
Log.v("wgeg3", userNickname + passWord)// 값 넘어오는거 확인
var dto = User(userNickname , passWord) // user dto 만든거임
var gson = Gson()
var objJson : String = gson.toJson(dto)
mservice.memberJoin(objJson).enqueue(object : Callback<ResponseBody> {
override fun onFailure(call: Call<ResponseBody>, t: Throwable) {
Log.d("@@@","실패 : {$t}")
callback.onDataNotAvailable("실패")
}
override fun onResponse(call: Call<ResponseBody>, response: Response<ResponseBody>) {
callback.onInfoLoaded("성공")
}
})
}
fun MemberLogin(userNickname: String, passWord: String, callback: MemberContract.InfoDataSource.LoadInfoCallback){
Log.v("wgeg3", userNickname + passWord)// 값 넘어오는거 확인
var dto = User(userNickname , passWord) // user dto 만든거임
var gson = Gson()
var objJson : String = gson.toJson(dto)
mservice.memberLogin(objJson).enqueue(object : Callback<ResponseBody> {
override fun onFailure(call: Call<ResponseBody>, t: Throwable) {
Log.d("@@@","실패 : {$t}")
callback.onDataNotAvailable("실패")
}
override fun onResponse(call: Call<ResponseBody>, response: Response<ResponseBody>) {
Log.v("llloggg", response.body().toString())
if(response.body()?.string().equals("success")){
callback.onInfoLoaded("성공")
}else{
callback.onInfoLoaded("정보없음")
}
}
})
}
}
Retrofit2 라이브러리를 이용하여 서버와 통신을 한다.
MemberLogin 메소드를 실행하는데 통신에 실패했을때는 onFailure에 반응이오고
통신에 성공했을때는 onRespose로 반응이 온다.
그리고 LoadInfoCallback 을 통해 실패했음과 성공했음을 Presenter에서 알 수 있게 콜백한다.
이렇게 전체적인 MVP 패턴의 흐름에 대해 알아보았다.
다음 포스팅에서는 어플리케이션을 개발하면서 유용했던 라이브러리 Picasso 라이브러리와
가볍지만 강력한 효과를 가져오는 Lottie 애니메이션 적용에 대해서 적어보겠다.