본문 바로가기
앱 제작/키워드 알림 앱

1.2.0 패치 노트 (버그 수정, 리팩터링)

by Kim Juhwan 2022. 6. 16.

 


 

 

약 10개월 만의 업데이트입니다.

지난 1년간 앱 개발 교육을 받느라 신경을 못썼습니다.. 죄송합니다 😢
대신 그동안 배운 내용으로 앞으로는 꾸준히 업데이트 해나가도록 하겠습니다.
바로 이번 패치 변경사항을 알아볼까요?

 

 

학교 공지사항 버그


  • 사용자의 시선 👀

제가 신경을 쓰지 못한 사이 학교 홈페이지가 리뉴얼 되었습니다.

겉으로 보이는 것 뿐만 아니라 안의 구성까지 싹 바뀌었기 때문에

이를 미처 대비하지 못한 앱에서 한동안 공지사항을 확인할 수 없었습니다.

이를 해결하기 위해 앱을 처음부터 다시 재구성했고, 현재는 정상적으로 공지사항을 확인할 수 있습니다.

 

  • 개발자의 시선 🧑🏻‍💻

API 주소도 바뀌고, 파라미터 요청 변수도 바뀌고

POST로 요청하던 게 GET으로 바뀌었습니다. (이건 애초에 POST이던 게 이상했죠. 공지사항을 불러오는 건데...)

심지어 response가 JSON에서 HTML로 바뀌었습니다. (HTML을 주면 클라이언트 입장에서 굉장히 불편합니다...)

그냥 싹 바뀌어서 코드를 새롭게 짜야했습니다.

 

 

스크롤 딜레이 제거


기존 방식(좌), 새로운 방식(우)

 

  • 사용자의 시선 👀

공지사항 스크롤 시 걸리는 딜레이 현상을 없앴습니다.

기존에는 마지막 게시물에 도달했을 때 로딩 애니메이션이 나타나고

잠시 뒤 새로운 게시물을 불러왔지만

이제는 로딩이라는 것을 느끼지 못하실 정도로 딜레이 현상을 없앴습니다.

 

  • 개발자의 시선 🧑🏻‍💻

Jetpack Paging 3을 적용했습니다.

API 요청 한 번에 가져오는 게시물의 개수가 10개라면

Paging 라이브러리는 자체적으로 그보다 미리 많은 개수의 게시물을 로드해오기 때문에

Progress bar를 보여줄 필요가 없어졌습니다.

 

더불어 기존 코드는 Recyclerview의 하단을 감지하고 Adapter에서 뷰 홀더 타입을 2개를 만드는 등

복잡한 로직을 View단에서 포함하고 있었지만 (기존 방식 링크)

이제는 Paging을 사용하기 때문에 코드가 간단해졌습니다.

 

 

학사일정 버그


  • 사용자의 시선 👀

학사일정 또한 홈페이지가 리뉴얼되면서 바뀌었기 때문에

2021년 학사일정이 계속 표시되고 있었습니다.

현재는 실시간으로 일정이 동기화되도록 변경하였습니다. 

 

  • 개발자의 시선 🧑🏻‍💻

처음에는 학사일정 API에 딱 일정 정보만 빠져있어서 굉장히 당황했습니다. 

개발자 모드에서 네트워크 주고받은 것들을 이것저것 조사해보니

일정 정보만 또 다른 API를 통해서 받고 있었습니다.

너무나 감사하게도 JSON으로 response를 주고 있어서 학사 일정을 편하게 가져다 사용할 수 있었습니다.

 

 

 

여기까지 사용자도 알아들을 수 있는 변경사항 내용이었습니다.
지금부터는 개발자만 알아들을 수 있는 이야기를 해볼까 합니다.

 

 

 

Clean Architecture & Dagger Hilt


업데이트를 하기 위해 10개월 만에 열어본 코드는 난장판 그 자체였습니다.

의존성 주입(DI)에 대한 개념이 없었기 때문에

무엇 하나 고치려면 줄줄이 사탕처럼 모든 것을 고쳐야 했습니다.

같은 문제가 재발하지 않도록 v1.2.0에서 Clean Architecture와 Dagger Hilt를 도입했습니다.

 

📦 com.anyang-yi
 ┣ 📂 data
 ┣ 📂 database
 ┣ 📂 network
 ┣ 📂 repository
 ┣ 📂 ui
 ┣ 📂 util

기존의 구조는 위와 같았습니다.

Entity와 DTO의 구분이 없었으며

일관성 없이 Repository, ViewModel, View 여기저기에 로직이 흩어져있고

정형화되어 있지 않은 구조라 알아보기 쉽지 않았습니다.

 

📦 com.anyang-yi
 ┣ 📂 data
 ┃ ┗ 📂 api
 ┃ ┗ 📂 db
 ┃    ┗ 📂 dao
 ┃ ┗ 📂 di
 ┃ ┗ 📂 mapper
 ┃ ┗ 📂 model
 ┃ ┗ 📂 paging
 ┃ ┗ 📂 repository
 ┣ 📂 domain
 ┃ ┗ 📂 model
 ┃ ┗ 📂 repository
 ┃ ┗ 📂 usecase
 ┣ 📂 present
 ┃ ┗ 📂 config
 ┃ ┗ 📂 service
 ┃ ┗ 📂 utils
 ┃ ┗ 📂 views

새롭게 적용한 Clean Architecture 구조입니다.

크게 data, domain, present로 나누었습니다.

사실상 코드를 처음부터 다시 짰다고 해도 과언이 아닐 정도로 갈아엎었습니다.

Clean Architecture와 Hilt를 사용하며 느낀 장점에 대해서는 따로 글을 작성할 예정입니다.

 

 

DataBinding


1.2.0 이전 버전에서는 데이터 바인딩을 사용하긴 했으나

아직 완전히 기술을 이해하지 못한 상태여서 findViewById를 대체하는 정도로만 활용을 했었습니다.

하지만 이번 버전부터는 xml에서 바인딩할 수 있는 코드는 전부 다 옮겨 View의 코드를 간소화했습니다.

당장은 코드가 줄어들었기 때문에 득인 것처럼 보이지만

오랜 시간이 지난 후 업데이트를 하려고 했을 때 한눈에 파악하기 더 어렵지는 않을지 좀 더 지켜봐야겠습니다.

 

 

예외 처리 & Result 패턴 사용


이번 업데이트에서 유독 신경 쓴 부분은 예외 처리였습니다.

사용자에게 단순히 기능을 구현한 앱이 아니라 완성도 높은 앱을 제공하고 싶었습니다.

기존에는 HTTP 통신을 해서 받아온 값이 정상적일 것이란 시나리오를 가지고 로직을 구성했고

에러가 발생할 때 사용자에게 충분한 안내 메시지를 제공하지 못했기 때문에 항상 마음에 걸렸습니다.

 

data class Result<out T>(
    val status: Status,
    var data: @UnsafeVariance T?,
    val message:String?
){
    companion object{
        fun <T> success(data: T?): Result<T> {
            return Result(Status.SUCCESS, data, null)
        }

        fun <T> error(msg: String, data: T?): Result<T> {
            return Result(Status.ERROR, data, msg)
        }

        fun <T> fail(): Result<T> {
            return Result(Status.FAIL, null, null)
        }

        fun <T> loading(data: T?): Result<T> {
            return Result(Status.LOADING, data, null)
        }
    }
}

그래서 적용한 것이 첫 번째로 Result 패턴입니다.

네트워크 통신 결과 값을 성공, 에러, 실패, 로딩 총 4가지의 상태로 구분함으로써 결과 패턴을 규격화했습니다.

 

@HiltViewModel
class NoticeViewModel @Inject constructor(
    private val getRecentUnivListUseCase: GetRecentUnivListUseCase
): ViewModel() {
    private val _recentUnivNoticeList = MutableLiveData<List<Univ>>()
    val recentUnivNoticeList: LiveData<List<Univ>> get() = _recentUnivNoticeList

    private val _problem = MutableLiveData<Result<Any>>()
    val problem: LiveData<Result<Any>> get() = _problem

    fun getRecentUnivNoticeList() {
        viewModelScope.launch {
            val result = getRecentUnivListUseCase()
            if(result.status == Status.SUCCESS) {
                result.data.let { _recentUnivNoticeList.postValue(it) }
            } else {
                _problem.postValue(result)
            }
        }
    }
}

try~catch로 처리도 해보고, API마다 if로 분기도 타봤지만

이 방법이 가독성이 가장 좋고 관리가 편하다고 느꼈습니다.

 

class ShortException: Exception("최소 2글자 이상 입력해주세요.")
class ExceedException: Exception("키워드는 10개까지 등록 가능합니다.")
class OutOfMatchException: Exception("한글과 숫자만 등록 가능합니다.")
class RegisteredException: Exception("이미 등록된 키워드입니다.")
object KeywordChecker {
    private const val KEYWORD_MIN_LEN = 2
    private const val KEYWORD_MAX_CNT = 10
    private var ps = Pattern.compile("^[0-9ㄱ-ㅎ가-힣]+$");

    fun check(keyword: String, registeredKeywordList: List<KeywordEntity>) {
        if(isShort(keyword)) throw ShortException()
        if(isExceedLimit(registeredKeywordList.size)) throw ExceedException()
        if(isOutOfMatch(keyword)) throw OutOfMatchException()
        if(isRegistered(keyword, registeredKeywordList)) throw RegisteredException()
    }

    fun searchCheck(keyword: String) {
        if(isShort(keyword)) throw ShortException()
        if(isOutOfMatch(keyword)) throw OutOfMatchException()
    }

    private fun isShort(keyword: String): Boolean {
        return keyword.length < KEYWORD_MIN_LEN
    }

    private fun isExceedLimit(count: Int): Boolean {
        return count > KEYWORD_MAX_CNT
    }

    private fun isOutOfMatch(keyword: String): Boolean {
        return !ps.matcher(keyword).matches()
    }

    private fun isRegistered(keyword: String, registeredKeywordList: List<KeywordEntity>): Boolean {
        return registeredKeywordList.any { it.keyword == keyword }
    }
}
            try {
                KeywordChecker.check(keyword, registeredKeywordList)
                viewModel.getSearchResult(keyword)
            } catch (e: Exception) {
                showToastMessage(e.message.toString())
            }

두 번째로 Custom Exception입니다.

기존에는 유효성 검사를 View단에 모든 로직을 넣고 if문을 태워 처리했었습니다.

유지 보수를 위해 역할을 최대한 나누고자 하여

Custom Exception과 유효성 검사 util을 따로 만들었고

View는 그저 실행 중 Exception이 발생하면 메시지를 출력하기만 하면 되도록 구현했습니다.

View는 이제 더 이상 유효성 검사를 어떻게 해야 할지,

어떤 메시지를 출력해야 할지에 대해 관여하지 않습니다.

 

이제 앱 사용 중 문제가 발생하더라도

아무런 안내 메시지 없이 빈 화면만을 보여주는 일이 없을 겁니다!

 

 

깃허브 관리


(앱에 대한 변경사항은 아니지만) 패치를 거듭하며 느낀 점은

내가 언제, 슨 이슈를, 어떻게 코드를 고쳤는지 기억나지 않는다는 것입니다.

깃허브를 제대로 관리해야겠다 생각이 들었습니다.

 

Milestones(= Sprint)
Kanban Board

 

우선 스프린트를 이용해서 일정 관리와 이번 버전에 해결할 목표를 잡았습니다.

칸반보드로는 진행 상황을 관리했습니다.

 

 

Issue / Pull requests

 

이슈를 등록해 해결해야 할 문제들을 잘게 쪼갰으며

Git-Flow 전략대로 브랜치를 나눠 Pull Request를 날리고 변경 사항들을 기재해두었습니다.

 

Release Tag

 

릴리즈에 태그를 걸어 어느 커밋까지가 이 버전에 해당하고

어떤 변경사항들이 있는지 확인할 수 있도록 했습니다.

 

 

 

 

 

그 외에도 BaseXXX 패턴 사용, BindingAdapter 사용 등 새롭게 바뀐 부분들이 많습니다.
이번 업데이트는 사용자 입장에서 보았을 때 단순 버그 수정이라 변경점이 많지 않지만
개발자 입장에서 보았을 때 거의 모든 코드가 바뀌어서 개발 기간이 2주나 걸렸습니다... 😭
사실 버그가 발생한 채로 업데이트를 너무 오랫동안 안 해서
많은 사용자분들이 이미 떠나셨을 거라 생각했는데,
첫날 무려 40여 명의 사용자분들이 앱 업데이트를 해주셨습니다. 감사합니다!!
앞으로 바뀔 아냥이도 많이 기대해주세요~ :D

 

 

반응형

댓글