이 포스팅은 안드로이드 코드 개선을 위한 문제 파악 후의 코드 개선 작업을 다루고 있습니다. 문제 파악과 선행 리펙터링 작업은 (1) 문제 파악과 (2) 함수 호출 포스팅을 확인해주세요!
관심사 분리는 개발에서 중요한 디자인 원칙 중 하나이다.
이는 코드를 작성할 때 다양한 책임을 가진 부분들을 분리하여 각각을 독립적으로 유지하는 것을 의미한다. 관심사 분리를 잘해야지만 유지보수성, 테스트 용이성, 재사용성 증가, 협업 용이성이 증가한다.
반대로 관심사 분리가 되어있지 않다는 것을 게임으로 비유하자면 파티에서 마법사
포지션이 딜과 힐과 탱킹을 하는 느낌이다. 이제는 딜 포지션을 강화하고 싶은데 모든 싸이클이 애매하게 딜과 힐과 탱킹이 섞여있어서 함부로 건드리다간 기존의 성능마저 망가질 수도 있는 것이다. 반면에 관심사 분리가 잘 되어 있는 코드는 포지션 분배가 잘 된 게임 파티 같아서 힐러에게는 힐러 아이템을 딜러에게 딜러 아이템을 주면 투자한만큼 성능이 잘 올라간다. 개발 용어로 다시 풀이하자면 확장 가능성이 높다는 뜻이다. 그러면 내 파티를 강화하기 위해 역할을 잘 분배해보도록 하자!
class EditorViewModel(private val databaseRepository: DatabaseRepository) : ViewModel() {
private val modelLineNumber = LineNumber()
private val _editTextStatement = MutableLiveData<EditText>()
val editTextStatement: MutableLiveData<EditText> = _editTextStatement
private val _textViewLineNumber = MutableLiveData<String>()
val textViewLineNumber: LiveData<String> = _textViewLineNumber
// Data binding with Activity_editor.xml #afterTextChanged()
fun onStatementChanged() {
val editText = editTextStatement.value
editText?.layout?.let { updateLineNumbers(it) }
}
// If line number changed, update model and render line number text view
private fun updateLineNumbers(it: Layout) {
if (it.lineCount != modelLineNumber.number) {
modelLineNumber.content = generateLineNumber(it.lineCount)
modelLineNumber.number = it.lineCount
_textViewLineNumber.value = modelLineNumber.content
}
}
// Generate line number like ""1/n2/n3/n ... count"
private fun generateLineNumber(count: Int): String {
val stringBuilder = StringBuilder()
for (i in 1..count) {
stringBuilder.append("$i\n")
}
return stringBuilder.toString()
}
// ...
}
자 다시 한번 문제의 코드를 보자. 리펙터링 전에 비하면 가독성이 많이 향상되었지만 아직 부족하다. 이제는 ViewModel이 사용자 인터페이스와 데이터 간의 중개자 역할에만 집중할 수 있도록 Line Number
로직을 분리해보자.
object LineNumberManager {
private val modelLineNumber = LineNumber()
// If line number changed, update model and render line number text view
fun updateLineNumbers(it: Layout): String {
if (it.lineCount != modelLineNumber.number) {
modelLineNumber.content = generateLineNumber(it.lineCount)
modelLineNumber.number = it.lineCount
}
return modelLineNumber.content
}
// Generate line number like ""1/n2/n3/n ... count"
private fun generateLineNumber(count: Int): String {
return buildString {
for (i in 1..count) {
append("$i\n")
}
}
}
}
class EditorViewModel(private val databaseRepository: DatabaseRepository) : ViewModel() {
private val _editTextStatement = MutableLiveData<EditText>()
val editTextStatement: MutableLiveData<EditText> = _editTextStatement
private val _textViewLineNumber = MutableLiveData<String>()
val textViewLineNumber: LiveData<String> = _textViewLineNumber
// Data binding with Activity_editor.xml #afterTextChanged()
fun onStatementChanged() {
editTextStatement.value?.layout?.let {
_textViewLineNumber.value = LineNumberManager.updateLineNumbers(it)
}
}
// ...
}
우선 LinenumberManager Object를 만들어주고 update 함수와 generate 함수를 분리해줬다. 이후에 EditorViewModel
의 코드를 수정해서 EditeText가 null이 아니면 Line Number를 Line Number Manager를 통해서 업데이트할 수 있도록 해주었다.
object LineNumberManager {
private val modelLineNumber = LineNumber()
// If line number changed, update model and render line number text view
fun updateLineNumbers(it: Layout): String? {
if (it.lineCount != modelLineNumber.number) {
modelLineNumber.content = generateLineNumber(it.lineCount)
modelLineNumber.number = it.lineCount
return modelLineNumber.content
}
return null
}
// Generate line number like ""1/n2/n3/n ... count"
private fun generateLineNumber(count: Int): String {
return buildString {
for (i in 1..count) {
append("$i\n")
}
}
}
}
class EditorViewModel(private val databaseRepository: DatabaseRepository) : ViewModel() {
private val _editTextStatement = MutableLiveData<EditText>()
val editTextStatement: MutableLiveData<EditText> = _editTextStatement
private val _textViewLineNumber = MutableLiveData<String>()
val textViewLineNumber: LiveData<String> = _textViewLineNumber
// Data binding with Activity_editor.xml #afterTextChanged()
fun onStatementChanged() {
editTextStatement.value?.layout?.let { layout ->
LineNumberManager.updateLineNumbers(layout)?.let { lineNumber ->
_textViewLineNumber.value = lineNumber
}
}
}
// ...
}
여기서 문제가 있었다. 함수를 분리하는 과정에서 TextView
의 업데이트가 afterTextChanged() 실행마다 일어나도록 바껴버렸다. 지난 포스팅의 generateLineNUmber() 호출 주기 제어와는 별개로 LineNumber만 계산하지 않을 뿐이지 같은 UI이더라도 계속 업데이트가 이뤄지고 있다. 이를 방지하기 위해서 updateLineNumbers()에서 만약 Line Number가 그대로라면 null을 반환하게 바꾸고 onStatementChanged()에서는 null safe 연산자를 통해서 UI 업데이트 여부를 결정한다.
class EditorViewModel(private val databaseRepository: DatabaseRepository) : ViewModel() {
private val _editTextStatement = MutableLiveData<EditText>()
val editTextStatement: MutableLiveData<EditText> = _editTextStatement
private val _textViewLineNumber = MutableLiveData<String>()
val textViewLineNumber: LiveData<String> = _textViewLineNumber
// Data binding with Activity_editor.xml #afterTextChanged()
fun onStatementChanged() {
editTextStatement.value?.layout?.let { layout ->
renderLineNumberView(layout)
}
}
// Render line number view when line number updated
private fun renderLineNumberView(layout: Layout) {
LineNumberManager.updateLineNumbers(layout)?.let { lineNumber ->
_textViewLineNumber.value = lineNumber
}
}
// ...
}
마지막으로 함수의 가독성을 위해서 onStatementChanged()
에는 editTextStatement에 대한 null safe 연산자를 이용한 검사 기능을 남겨두고 UI 업데이트 로직은 renderLineNumberView()
로 분리해주었다.
이번 리펙터링은 아예 로직 자체를 분리시켰기 때문에 EditorViewModel의 코드는 상당히 줄어들었다. 코드만 줄어든 것이 아니라 성능 향상의 효과도 있었다. 어쩌다보니 관심사 분리와 UI 업데이트 주기 튜닝을 같이 해줬는데, Line Number 로직 전체를 분리한 덕분에 EditorViewModel의 코드는 상당히 줄어들었다. 리펙터링 전체로 향상된 부분을 정리하면 아래와 같다.
Layout(EditText)
가 변경될 때로 수정했다.NEW!
EditorViewModel의 관심사 분리: 사용자 인터페이스와 데이터 간의 중개자 역할에만 집중할 수 있게 되었다. 두번에 걸친 리펙터링의 결과 중 가장 만족스러운 부분은 아무래도 관심사 분리와 UI 업데이트 주기 조절이 아닌가 싶다. 리펙터링 전후의 함수 호출 횟수를 로그로 찍어봤는데, 간단한 CREATE문 생성을 위해서 리펙터링 전
에는 120회 호출되면 함수들이 리펙터링 후
에는 5회 호출로 크게 감소했다. Compose 같은 경우에는 Recomposition
의 중요성을 알고 있었기에 조심하겠지만 XML은 아무 생각 없이 기능만 구현해버렸던 거 같다. 앞으로는 UI 업데이트도 하나 하나 신경 쓴다는 생각으로 기능을 구현해야겠다.
이렇게 리펙터링을 3개의 포스트에 걸쳐서 진행해보았다. 아직까지는 부족한게 많은 코드다. 테스트 용이성과 예외 처리가 잘 되어있는지 확신이 없다. GitHub Actions의 Unit Test에 문제가 있어서 Test 코드를 작성하지 않고 있는데 문제가 해결되는대로 추가할 예정이니 앞으로의 리펙터링은 테스트 용이성 확보를 목표로 할 듯하다. 이전 글을 보면 알겠지만 SQL문 실행 로직이 EditorViewModel에 일부 남아있어 이와 비슷하게 포스팅을 하려하니 많은 관심! 부탁한다🙏🙏