안드로이드 코드 개선하기 (2) 함수 호출

Lee Sanghyun·2024년 4월 18일
1

SQLI

목록 보기
5/7
post-thumbnail

이 포스팅은 안드로이드 코드 개선을 위한 문제 파악 후의 코드 개선 작업을 다루고 있습니다. 문제 파악 부분은 안드로이드 코드 개선하기 (1) 문제 파악 포스팅을 확인해주세요!

🚀 Refactoring Start

우선 지난 포스팅에서 파악한 문제점들이다. 조바심 내지말고 차근차근 하나씩 수정해보도록 하자.

💥 문제 분석 (1)

    private val lineNumberModel = LineNumber("")
    var statementEditView = MutableLiveData<EditText>()
    val lineNumberView = MutableLiveData<String>()
  • 접근지정자와 변수명:
    • 변수명을 보고 의도를 파악하기 어렵다. 가독성
    • ViewModel의 상태가 캡슐화 되어 있지 않다. 객체지향

🛠️ 해결 방법 (1)

    private val modelLineNumber = LineNumber()

    private val _editTextStatement = MutableLiveData<EditText>()
    val editTextStatement: MutableLiveData<EditText> = _editTextStatement

    private val _textViewLineNumber = MutableLiveData<String>()
    val textViewLineNumber: LiveData<String> = _textViewLineNumber
  • LiveData와 MutableLiveData는 XML과 데이터바인딩되어 있기 때문에 정확히 어떤 컴포넌트와 바인딩되어있는지 명확히 보여주기 위해서 "컴포넌트 종류 + 용도"로 변경해주었다.
  • modelLineNumber는 기본 매개변수를 명시해서 보기 싫은 ""를 제거해주었다.
  • Private 변수임을 보여주기 위해서 _을 변수명 앞에 붙여주었다.
  • _editTextStatement_textViewLineNumber는 ViewModel 내부에서만 수정하도록 Private으로 설정하고 textViewLineNumber는 데이터바인딩한 컴포넌트를 read-only로 관찰하기 위해서 LiveData로 선언해주었다.
    • 다만 editTextStatement는 Activity에서 ViewBiding으로 넘겨받았기 때문에 MutableLiveData로 선언해주었다. 임의로 MutableLiveData의 값에 접근하지 않도록 주의해야한다. 권장할만한 방법은 아닌 것 같지만 곧 XML에서 Compose로 변경할 예정이기에 우선은 이렇게 변경해주었다.

💥 문제 분석 (2)

	// Activity_editor.xml과 data binding #EditText의 afterTextChanged()
    fun onStatementChanged() {
        val editText = statementEditView.value
        val layout = editText?.layout
        layout?.let {
            lineNumberModel.content = generateLineNumber(it.lineCount)
            lineNumberView.value = lineNumberModel.content
        }
    }
  • onStatementChanged():
    • generateLineNumber() 함수를 과하게 많이 호출한다. 자원 낭비
    • 너무 많은 역할을 수행하고 있다. 관심사 분리 부족

🛠️ 해결 방법 (2)

    // Data binding with Activity_editor.xml #EditText afterTextChanged()
    fun onStatementChanged() {
        val editText = editTextStatement.value
        editText?.layout?.let { updateLineNumbers(it) }
    }

    private fun updateLineNumber(it: Layout) {
        if (it.lineCount != modelLineNumber.number) {
            modelLineNumber.content = generateLineNumber(it.lineCount)
            modelLineNumber.number = it.lineCount
            _textViewLineNumber.value = modelLineNumber.content
        }
    }

우선은 Data Class인 LineNumber를 통해서 Line Count를 관리해주도록 바꿨다. 그리고 함수를 Null 검증과 Update로 분리해주면서 줄 수가 변했을 때만 generateLineNumber() 메소드를 호출하도록 바꿔주었다. 이렇게 해준 뒤 Log를 비교해보자.

	Log.i("CALL_UPDATE", "${++count}")

좌측의 화면의 글자를 타이핑하는 동안 리펙터링 전에는 총 41번의 generateLineNumber() 함수 호출이 있었고 리펙터링 후에는 10번의 함수 호출이 있었다. 이전에는 한글자 입력할 때마다 함수를 호출했는데 지금은 줄 수의 변화에 따라 호출하는 것이라 상당히 많은 차이가 있었다.


💥 문제 분석 (3)

	// SQL문이 쿼리인지 검증. 즉 SELECT로 시작하는지 확인
    private fun isQueryStatement(statement: String): Boolean {
        return statement.startsWith("SELECT") or statement.startsWith("select")
    }
  • isQueryStatement():
    • "Select" "SELect" 같은 키워드는 제대로 확인하지 못한다. 의도 실패
    • 함수명이 의도를 명확하게 보여주지 못한다. 가독성

🛠️ 해결 방법 (3)

	// SQL문이 쿼리인지 검증. 즉 SELECT로 시작하는지 확인
    private fun isSelectStatement(statement: String): Boolean {
        return statement.startsWith("SELECT", ignoreCase = true)
    }

ignoreCase를 적용해주고 함수명을 변경해주었다. seLectSeleCT처럼 대문자와 소문자를 혼합해서 사용하는 경우를 생각하지 못 해서 생긴 문제였다.


✨ Refactoring Result

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
        }
    }

    // View binding with Activity_editor.xml, Check Editor Activity
    fun execStatement(): Any? {
        return _editTextStatement.value?.let {
            if (isQueryStatement(editTextStatement.value.toString())) {
                databaseRepository.execQuery(it.text.toString())
            } else {
                databaseRepository.execStatement(it.text.toString())
            }
        }
    }

    // Check if a statement starts with "SELECT" ignore case
    private fun isSelectStatement(statement: String): Boolean {
        return statement.startsWith("SELECT", ignoreCase = true)
    }

    // 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()
    }
}

리펙터링 후에 오히려 코드 수가 늘어났다. onStatementChanged()의 관심사를 분리하기 위해서 별도의 함수를 구현한게 원인이다. 코드 수가 늘어난 점을 제외하면 많은 부분이 향상되었다. 정리해보자면 아래와 같다.

  • 변수명의 가독성 향상: 명확한 쓰임새를 파악하기 쉬워졌음
  • ViewModel의 캡슐화 강화: LiveData를 read-only로 추가하고 MutableLiveData를 private으로 선언함으로써 클래스 외부에서 ViewModel의 데이터에 접근할 수 없게 했다.
  • UI 업데이트 빈도 조정: Line Number 업데이트 로직의 발동 조건을 Layout(EditText)가 변경될 때로 수정했다.
  • 함수 관심사 분리: 하나의 함수가 여러 역할을 수행하는 것을 방지하기 위해 코드를 분리해줬다. 이를 통해서 테스트 용이성이 증가했다!
  • 함수 오류 수정: 쿼리 검증문을 대소문자 구분 없이 검증할 수 있도록 ignoreCase를 true로 설정해주었다.

이렇게 해서 리펙터링 1차를 마무리했다. 관심사 분리를 위해서 LineNumber 로직 전반을 별도의 클래스로 분리하려 했지만, 포스팅의 분량이 너무 길어질까봐 후에 다루도록 하겠다. 추천 노래를 남기면서 글을 마무리한다. 다들 안녕😉


SQLI 프로젝트의 GitHub Repository가 궁금하다면? 클릭


📚 참고

profile
개미 같이 일하는 안드로이드 개발자💻

0개의 댓글

관련 채용 정보