서두
Coroutine을 이용하여 비동기적으로 서버로 부터 데이터를 다운받고 그 결과를 화면에 뿌려주는 기능을 구현한다고 가정했을 때 가장 대표적인 디자인 패턴인 MVC, MVP, MVVM 을 어떻게 구현하는 지를 간단한 샘플코드를 이용하여 나타 내고자 합니다. 먼저 디자인 패턴 중 MVC 부터 살펴보도록 하겠습니다.
실행 화면
앱에 대해 간단히 설명하자면 서버로 부터 HTML 문서를 다운받고 문서로 부터 이미지-제목을 파싱하여 RecyclerView에 뿌려주는 갤러리 앱입니다. 비동기 처리를 위해 저는 Coroutine을 사용했습니다.
샘플 코드
아래 Repository를 참고바랍니다.
git checkout mvp
MVC
MVC 는 Model-View-Controller의 약자로써 3개의 집합으로 구분합니다.
View
화면을 구성하는 컴포넌트입니다.
위 샘플코드에서 View는 Layout Xml, RecyclerView Adapter인 ImageDataAdapter가 있습니다.
class ImageDataAdapter(private val context: Context) :
RecyclerView.Adapter<ImageDataAdapter.ViewHolder>() {
private val imageDataList = mutableListOf<ImageData>()
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val image: ImageView = itemView.findViewById(R.id.image)
val title: TextView = itemView.findViewById(R.id.title)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val convertView =
LayoutInflater.from(parent.context).inflate(R.layout.list_item, parent, false)
return ViewHolder(convertView)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val imageData = imageDataList[position]
holder.apply {
Glide.with(context).load(imageData.imageUrl)
.override(150, 150)
.into(image)
title.text = imageData.imageTitle
}
}
override fun getItemCount() = imageDataList.size
fun add(imageData: ImageData) {
imageDataList.add(imageData)
notifyItemInserted(imageDataList.size)
}
}
ImageDataAdapter 는 add() 를 통해 새로운 item을 받아서 Recycler view에 계속 추가를 하는 역할을 합니다.
Model
데이터를 구성하는 컴포넌트입니다.
Image Url과 Image Title를 저장하는 data class와 data class 를 생성하는 data provider 가 있습니다.
ImageData
data class ImageData(val imageUrl : String, val imageTitle : String)
ImageDataProvider
class ImageDataProvider {
private val baseUrl = "https://www.gettyimagesgallery.com/collection/sasha/"
fun get(coroutineScope: CoroutineScope): ReceiveChannel<ImageData> {
return coroutineScope.produce(IO) {
try {
val url = URL(baseUrl)
val conn: HttpURLConnection = url.openConnection() as HttpURLConnection
if (conn != null) {
conn.connectTimeout = 2000;
conn.useCaches = false;
if (conn.responseCode
== HttpURLConnection.HTTP_OK
) {
// 데이터 읽기
val br = BufferedReader(InputStreamReader(conn.inputStream, "euc-kr"))
br.lineSequence()
.filter { it.contains("img class=") }
.forEach {
val pattern = Pattern.compile("\"(.*?)\"")
val matcher = pattern.matcher(it)
var index = 0
var imageUrl = ""
var title = ""
while (matcher.find()) {
val token = (matcher.group(1))
if (index == 1) {
imageUrl = token
} else if (index == 2) {
title = token
val imageData =
ImageData(imageUrl = imageUrl, imageTitle = title)
send(imageData)
}
index += 1
}
}
br.close(); // 스트림 해제
}
println("=========end=========")
conn.disconnect()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}
HTTP 통신 후 HTML 문서를 파싱하여 ImageData 들을 Coroutine
의 producer
를 이용하여 하나씩 생산을 하여 Controller에게 전달을 합니다.
네트워크 통신, 문서 파싱하는 동작은 시간이 오래 걸리는 작업이기 때문에 Main Thread 가 아닌 Working Thread를 통해서 수행이 되어야 합니다.
Controller
View나 Anroid Framework 부터 이벤트를 받고 Model을 업데이트하고 업데이트 된 Model을 이용하여 View를 업데이트하는 모듈입니다.
Andoid MVC 에서 Controller는 Activity가 담당을 합니다.
MainActivity
class MainActivity : AppCompatActivity(), CoroutineScope {
lateinit var job: Job
private lateinit var recyclerView: RecyclerView
private lateinit var viewAdapter: ImageDataAdapter
private lateinit var viewManager: RecyclerView.LayoutManager
private var dataCount = 0
override val coroutineContext: CoroutineContext
get() = Main + job
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
job = Job()
viewManager = LinearLayoutManager(this)
viewAdapter = ImageDataAdapter(this)
recyclerView = this.findViewById<RecyclerView>(R.id.image_title_list).apply {
layoutManager = viewManager
adapter = viewAdapter
}
launch {
dataCount = 0
findViewById<TextView>(R.id.counter).text = "Image Count : $dataCount"
val channel = ImageDataProvider().get(this)
println("channel status ${channel.isClosedForReceive}")
channel.consumeEach {
withContext(Main) {
dataCount++
findViewById<TextView>(R.id.counter).text = "Image Count : $dataCount"
viewAdapter.add(it)
}
}
println("channel status ${channel.isClosedForReceive}")
}
}
override fun onDestroy() {
super.onDestroy()
job.cancel()
}
}
여기서 Controller가 받은 이벤트는 Activity가 생성이 완료가 되었다는 onCreate()
입니다.
onCreate()
에서 ImageDataProvider
를 생성하고 Corouine의 Received Channel 를 통해 ImageData
를 공급을 받고 RecyclerView를 업데이트 합니다.
중요한 점은 RecyclerView를 업데이트 할 때에는 Working Thread 에서 Main Thread 로 스위칭을 해야 정상적으로 업데이트가 가능합니다.
MVC 패턴의 장점
- 구조가 단순하고 구현이 쉽습니다. 따라서 개발 기간이 비교적 짧습니다. 구조가 단순하고 기능이 비교적 적은 case에 적합합니다.
MVC 패턴의 단점
- Contoller가 Android Framework 또는 View에 강력하게 종속이 되어 있습니다. 그렇기에 Controller를 다른 View로 교체가 불가능하고 Android Framework가 변경이 되면 Controller에도 영향이 발생할 수도 있습니다.
- 위에서 언급한 종속성 때문에 Controller 로직에 대해서 테스트 코드를 작성하기가 힘듭니다.
- 기능이 추가가 될수록 Controller가 복잡해지고 코드 사이즈가 커지므로 유지 보수를 어렵게 합니다.
마무리
MVC는 구조가 단순하고 구현하기가 쉽다는 장점이 있지만 기능이 추가가 될 수록 유지보수의 어려움이 발생을 합니다. 따라서 위 단점을 극복한 것이 MVP 패턴입니다.
다음 포스트에서 MVP에 대해 알아 보도록 하겠습니다.
댓글남기기