240426 TIL - 내일배움캠프 숙련주차 팀프로젝트 완료

DoDoBest

·

2024. 4. 27. 00:34

https://github.com/AndroidJeong/NBC_TwoOfUs

 

GitHub - AndroidJeong/NBC_TwoOfUs

Contribute to AndroidJeong/NBC_TwoOfUs development by creating an account on GitHub.

github.com

 

내일배움캠프 숙련주차 팀프로젝트가 끝났습니다. 이제 심화 주차(개인 학습 2주 + 팀프로젝트 2주), 실전 프로젝트(6주)가 남았는데, 이번 팀프로젝트를 통해 느낀점을 정리하고자 합니다.

 

1. 공통으로 사용하는 데이터는 엄격하게 관리하자

커리큘럼 상에서 Repository를 배우지 않아 사용할 수 없었기에 어쩔 수 없이 Object class를 이용해 데이터를 관리했습니다. 이때, 데이터의 변화를 추적하기 쉽도록 데이터를 private으로 감추고 함수를 통해서만 간접적으로 접근할 수 있도록 했는데, 코드의 논리적 오류 원인을 추적하는데 많은 도움이 됐습니다.

다만, 이번 팀프로젝트는 규모가 비교적 매우 매우 작았기 때문에 1시간 또는 3시간 내에 오류를 해결할 수 있었습니다. 만약 프로젝트 코드의 규모가 조금 더 컸더라면 기한 내에 코드를 완성하기 위해 논리적 오류만을 해결하기 위한 안티패턴 코드를 작성했을지도 모릅니다.

 

그래서 심화 주차 개인 학습 기간 동안 데이터의 논리적 오류를 사전에 검증하기 위한 방법들을 생각해보고, 팀프로젝트에서 검증하는 과정을 거쳐, 실전 프로젝트에서 적용하기 위한 구조를 만들어볼 계획입니다.

 

2. LiveData를 Event Class로 wrapping하는 이유

LiveData를 여러 번 관찰하는 것을 막기 위해 Event class로 wrapping하는 코드를 종종 쓰곤 했었는데, 이번 팀프로젝트를 진행하며 필요성을 몸소 느꼈습니다.

 

https://github.com/android/architecture-samples/blob/dev-dagger/app/src/main/java/com/example/android/architecture/blueprints/todoapp/Event.kt

open class Event<out T>(private val content: T) {

    @Suppress("MemberVisibilityCanBePrivate")
    var hasBeenHandled = false
        private set // Allow external read but not write

    /**
     * Returns the content and prevents its use again.
     */
    fun getContentIfNotHandled(): T? {
        return if (hasBeenHandled) {
            null
        } else {
            hasBeenHandled = true
            content
        }
    }

    /**
     * Returns the content, even if it's already been handled.
     */
    fun peekContent(): T = content
}

 

이 class를 도입하게 된 이유는, 화면 회전 시 이전에 관찰되었고, 더 이상 최신화 되지 않은 데이터를 또 다시 관찰하는 문제를 방지하기 위함이였습니다.

저희 앱에서는 2개의 Fragment가 같은 LiveData를 관찰하고 있었는데, 상황에 따라 한쪽 Fragment만 데이터를 관찰하거나, 두 개의 Frament가 Event class에 동시에 접근해 데이터를 둘 다 사용하는 경우가 발생했습니다.

그래서 아래와 같이 Event 클래스 생성자에 최대 관찰할 수 있는 횟수를 지정했습니다. 두 Fragment가 동시에 접근할 경우 관찰 횟수가 1번으로 기록되는 경우를 방지하기 위해 synchronized block으로 감싸줬습니다.

 

open class Event<out T>(private val content: T, private var count: Int = 1) {

    @Suppress("MemberVisibilityCanBePrivate")
    var hasBeenHandled = false
        private set // Allow external read but not write

    /**
     * Returns the content and prevents its use again.
     */
    fun getContentIfNotHandled(): T? {
        // LiveData를 두 곳에서 동시에 접근하는 경우에 count 처리를 정확하게 하기 위해 synchronized 막음
        return synchronized(this) {
            if (hasBeenHandled) {
                null
            } else {
                if (--count <= 0) {
                    hasBeenHandled = true
                }
                content
            }
        }
    }

    /**
     * Returns the content, even if it's already been handled.
     */
    fun peekContent(): T = content
}

 

3. 1개의 Fragment를 여러 화면을 표시하기 위해 사용하는 것을 지양하자

아래의 구현 조건을 보고 상세 정보 화면과 마이 페이지 UI가 비슷하기 때문에 동일한 Fragment로 구현하기로 했습니다.

 

 

문제가 된 부분은 상세 정보 화면으로 표시하는 연락처 정보가 사용자인 경우입니다. 연락처를 표시하는 Fragment에서 상세 정보 화면 Fragment를 띄울 때, replace가 아닌 add로 구현했습니다. 연락처 목록 중 사용자 자신의 정보에 진입하는 경우, LiveData를 세 곳에서 관찰하는 상황이 발생합니다.

연락처 상세 화면과 사용자 정보 화면이 동일한 Fragment로 되어 있다는 것은 알고 있었지만, 이로 인해 LiveData를 세 곳에서 관찰하게 되고, 이것이 의도하지 않은 UI 업데이트 누락 문제를 야기하기란 것을 알아내는 데는 시간이 오래 걸렸습니다.

그래서 앞선 2번에서 Event를 최대 2번 관찰할 수 있는 경우를 만들었는데, 자신의 상세 정보 화면에 진입하는 경우 Event를 최대 3번 관찰할 수 있도록 수정하여 세 곳에서 모두 UI가 업데이트 되도록 수정했습니다.

 

 

4. Fragment의 생성자로 데이터를 넘기면 안 된다.

DialogFragment를 이용해 아래와 같이 사용자 정보를 수정할 수 있도록 구현했습니다.

 

 

이때 아래와 같이 생성자를 통해 Fragment에게 데이터를 전달하는 로직이 있었습니다. Fragment의 생성자에 있는 값은 화면 회전 등으로 인해 Fragment가 파괴되고 재생성될 때 복구되지 않습니다. 따라서 생성자가 아닌 Fragment arguments를 통해 전달하도록 코드를 수정했습니다.

 

 

5. 코드리뷰의 중요성을 깨달았습니다.

저희 팀은 5명으로 구성되어 있고, 2명 이상의 승인을 받아야지만 PR을 머지할 수 있도록 설정했습니다. 머지 되지 않은 PR을 확인할 때, 모든 코드를 확인했는데, 이미 승인되어 머지된 경우에는 팀원 분들이 확인을 다했다는 생각에 확인하지 않았습니다. 그래서 4번에서 있었던 생성자로 데이터를 넘기는 코드가 합쳐지는 것을 발견하지 못하고 넘어갔었습니다.

 

다음 팀프로젝트에서는 전원의 승인과 더불어 모든 코드를 확인하는 것을 의무화하기 위해 코드에 대한 코멘트를 2개 이상 다는 것을 규칙으로 정해볼 계획입니다.

 

6. Bundle key 값을 관리하는 규칙의 중요성을 깨달았습니다.

연락처 상세 화면에서 편집 화면인 DialogFragment에게 Bundle로 연락처 정보를 전달했습니다. 그런데, Bundle 데이터를 아무리 전달해도 DialogFragment에서 Key 값으로 데이터를 확인해보면 null이 반환됐습니다.

이유는 중복된 이름의 Bundle Key가 여러 클래스에 존재했고, 이 키 값을 Class.BUNDLE_KEY와 같이 사용하는 것이 아니라 BUNDLE_KEY와 같이 import 하는 과정에서 일치하지 않는 Key 값을 사용하는 것이 원인이였습니다.

 

그래서 이러한 실수를 방지하기 위해 상수나 다른 클래스의 함수를 호출할 때, 해당 값이나 함수를 import하는 것이 아니라 포함된 클래스를 import 하는 습관을 가지려고 합니다.