본문 바로가기
활동/우아한테크캠프5기

우아한 테크캠프 3주차 - 2

by ESHC 2022. 7. 26.

2주차에 이어 팀 프로젝트를 진행하였다. 구현과 리팩토링을 통해서 배운 부분과 수업에서 배운 내용을 정리하였다.

 


Domain Layer

Domain Layer는 복잡한 비즈니스 로직, 또는 여러 ViewModel에서 재사용되는 간단한 비즈니스 로직의 캡슐화를 담당한다. 모든 앱에서 필수적인 요구사항이 아니기 때문에 필요한 경우에 Domain Layer를 선택적으로 사용해야 한다.

UseCase

Domain Layer에는 UseCase라 불리우는 클래스로 구성될 수 있는데 각 UseCase는 하나의 기능을 담당해야 하고 각각 담당하고 있는 단일 작업에 따라 이름이 지정된다. 작은 단위로 UseCase를 나눠서 복잡도가 올라갈 수도 있지만 각 객체의 책임을 명확하게 분리하는 것이 관리하기 편하기 때문에 UseCase를 작은 단위로 나눈다.

 

일반적인 앱 아키텍처에서 UseCase는 UI Layer의 ViewModel과 Data Layer의 Repository 사이에 적합하다. 또한 UseCase는 재사용 가능한 로직을 포함하고 있어서 다른 UseCase에 의해 사용되어질 수 있다.

참고 : https://developer.android.com/jetpack/guide/domain-layer

RecyclerView

ViewHolder

각 View를 보관하는 Holder 객체. 스크롤이 넘어감에 따라 재사용이 가능하기 때문에 앱의 효율을 향상시킬 수 있다. 따라서 재사용할 때 원하는 데이터나 뷰의 형태를 위해 초기화해주는 코드를 넣어주는 것이 바람직하다. 

ViewHolder의 itemView는 보통 item_##.xml로 만들어주는 아이템의 레이아웃의 뷰로 생각하면 될 것 같다.

요구사항에 따라 ViewHolder의 ViewType에 따라 다르게 뷰를 그려줘야 하는 경우가 있다. 이 때 sealed 클래스를 이용해서 구현할 수 있다. 

sealed class UIModel {
    object Header : UIModel()

    object Body : UIModel()

    class NotificationModel(val notificationItem: NotificationItem) : UIModel()
    
    class IssueModel(val issueItem: IssueItem) : UIModel()
}

data class NotificationItem(val title : String, val commentsNum : String)

data class IssueItem(val title : String, val issueNum : String)

위와 같이 UIMode를 SealedClass로 구현하고 Adapter내에서 getItemViewType와 onCreateViewHolder, onBindViewHolder를 클래스에 맞게 구현해준다.

 

참고 : https://betterprogramming.pub/android-recyclerview-with-kotlin-sealed-classes-6d2985aac3e5

 

디자인 패턴

 

특정 문맥에서 공통적으로 발생하는 문제에 대해 재사용 가능한 해결책. 생성, 구조, 행동 패턴으로 크게 분류될 수 있다.

 

참고 : https://ko.wikipedia.org/wiki/소프트웨어_디자인_패턴

 

어댑터패턴

어댑터 패턴(Adapter pattern)은 클래스의 인터페이스를 사용자가 기대하는 다른 인터페이스로 변환하는 패턴으로, 호환성이 없는 인터페이스 때문에 함께 동작할 수 없는 클래스들이 함께 작동하도록 해준다.

*조금 더 이해가 필요해보임

 

참고 : https://ko.wikipedia.org/wiki/어댑터_패턴

 


Multiple Api Call

이번 프로젝트에서 꽤 오랜 시간 고민하였던 문제였다. API 호출을 통해 리스트 형태의 아이템 목록을 받은 후 각 아이템마다 API 호출을 해야하는 상황이었다. 각각의 API를 단순 for문을 통해 호출할 경우 동기적으로 실행되어 시간이 오래 소요되었다. 

API를 한번에 요청하고 한번에 응답 받아야한다

이러한 문제는 Coroutine - async, await 사용해서 해결이 가능하였다. async는 새 코루틴을 시작하고 await라는 정지 함수로 결과를 반환하도록 한다. 각 API async 통해 거의 동시에 호출을 하고 await 통해 값을 받으면 시간을 단축시킬 있다. 실제 수정한 결과 약 3초 정도의 시간 단축을 할 수 있었다.

또한 awaitAll() 이용하여 async 대한 응답값들을 리스트 형태로 받을  있었고,  zip 함수를 통해 기존의 item목록과 응답받은 ItemInfo목록을 한번에 처리할  있었다. 아래는 프로젝트에서 실제 사용한 코드를 일부 변경한 코드이다.

 

val items = getItems.getOrThrow()
val itemInfo = items.map {
    async {
        dataSource.geItemInfo().getOrThrow()
    }
}.awaitAll()
Result.success(items.zip(itemInfo) { item, itemInfo ->
    item.apply {
        item.description = itemInfo.description
    }
}.toMutableList())

 

목록에서 아이템 삭제를 할 때 고려할 사항?

RecyclerView를 사용하면서 페이징 처리를 할 경우, 목록에서 아이템을 삭제했을 때 주의해야 할 점이 있다. 지금까지의 목록의 아이템 중 하나 이상을 삭제하기 위해 바로 서버에게 알린다면 서버에서 삭제 처리를 하게 되고 그 때 다시 그 다음 페이지의 데이터를 요청한다면 삭제했던 아이템의 수만큼 누락된 데이터를 받게 된다. 

즉 삭제나 추가를 바로 요청해서 서버상의 데이터 목록과 현재 안드로이드 앱에서 보여지고 있는 데이터 목록이 다를 경우 페이지를 다음 페이지를 불러올 때 문제가 생길 수 있다. 

 

삭제 작업 후 바로 다음 페이지 요청을 할 경우에 문제가 생기므로 삭제 작업을 바로 하지 않고 삭제할 아이템을 모아두었다가 다른 화면이나 새로고침을 할 때 삭제할 아이템들을 한번에 처리하고 해당 목록을 첫 페이지로 새로 불러오는 식으로 처리하였다. 삭제 작업 이후 조회 작업이 이뤄져야 하므로 두 작업은 동기적으로 처리되어야 한다고 생각하여 이번에도 async와 await를 사용하고  onStop()에서 삭제 작업과 목록 조회 작업을 구현하였다.

하지만 해당 화면에서 바로 종료되어 프로세스가 종료되는 경우에는 처리 작업이 제대로 이뤄지지 않는 문제가 생겼다. 이러한 경우 Foreground Service로 구현하면 서비스가 죽지않고 남은 처리 작업을 할 수 있다고 생각하여 이러한 방향으로 구현할 생각이고 시간이 나는대로 개발을 진행해야겠다.

 

이번 프로젝트에서는 이 부분을 고려하지 못했다가 다른 교육생분의 발표를 들으면서 뒤늦게 파악한 내용이었다. 다음 프로젝트에서는 이러한 부분을 신경쓰고 개발할 필요가 있고, 더 세밀하게 요구사항을 분석하고 의도한 대로 앱이 잘 작동하는지 확인할 필요가 있다.

 

일관성있는 프로젝트

이번 프로젝트는 두 명으로 구성된 팀으로 진행되었다. 서로 코딩 컨벤션을 협의하고 브랜치 전략을 세워서 협업을 진행하였지만 각자 분담한 기능 개발에서 구현하는 방식이 조금씩 차이가 있는 부분이 있었다. 개발의 큰 맥락은 같지만 세밀한 구현 방식이 차이가 있었기 때문에 관련하여 이슈나 수정 사항이 발생할 경우 각자 또 다르게 수정하거나 처리를 해야하는 문제가 생기곤 했다. 이러한 문제가 발생하지 않으려면 처음부터 세세한 구현 방식까지 정하거나 해당하는 부분을 개발할 때 확실하게 논의를 하고 정해야하지 않을까 싶다. 

 

댓글