TIL

[TIL] Kotlin 프로젝트 리팩토링

정상호소인 2023. 11. 7. 23:59

 

Topic =  프로젝트 리팩토링 - 진행 중

 

 

 


 

Why - 아직 리팩토링에 대해서 어떤 방식이 좋은진 모르지만 이번 프로젝트 진행하면서 다시 생각하게 된 내용들이 많아서 정리.

 

1. 초기에 디자인 패턴 등을 활용하여 객체지향적인 구조를 잘 만들어보자

  • 기존의 문제점
    • 기능 구현에만 급급하여서 작성한 코드 전반적으로 유지보수성이 굉장히 낮았다. 이로인해 리팩토링을 하기 위해 구조 자체를 바꾸어야 하는 문제등이 발생했다. 아래에 발생한 몇가지 케이스의 문제를 간단하게 적어두고 하단 리팩토링 코드에 추가로 작성.

 

Case1

  • NaverMap API를 통해 지도를 사용하는데, 하나의 MapViewFragment로 관리하여 게시글 작성자의 경우 지도 클릭시 마커를 생성하여 마커 위치 정보를 담고, 담긴 마커 위치 정보와 Geocoder를 통해 얻은 정보를 우측 상단에 설정완료 버튼을 통해 저장할 수 있도록 하고, 작성자 외의 사용자의 경우 별도로 지도 클릭 이벤트가 없으며 설정완료 버튼또한 필요가 없다. 
    • 1. OOP 원칙을 지키지 않아 유지보수성 저하 등 여러가지 불편함이 생긴다.
    • MapViewFragment 안에서 일반 사용자와 작성자를 구분하여 로직을 작성하려고 하니 일반 사용자에게 만 필요한 특정 기능을 추가할 때 마다 이를 별도로 구분시켜주어야 하는 번거로움이 발생했다

 

 

Case2

  • 하나의 ViewModel 안에서 프로젝트 전반적으로 사용하는 로직들을 담고 SharedViewModel로 사용해서 MyPage의 경우 필요하지 않은 기능들이 포함된 ViewModel을 사용하고 있는데 아래와 같은 문제가 생겼다.
    • 1. 데이터 로직을 하나의 ViewModel에서 관리하므로 작성할 때는 간편하게 작성했는데 큰 프로젝트가 아님에도ViewModel이 비대해져서 관리하기가 어려웠다.
    • 2. 테스트 코드를 아직 작성하진 않았지만 ViewModel 로직 각각에 대한 단위 테스트가 어려워지는 문제가 생긴다고 한다.
    • 3. MyPage에 필요한 로직과 Home에 필요한 로직 등 서로 관련이 없는 로직을 포함하고 있어 코드 유지보수성이 떨어지고, 가독성 또한 좋지 않던 문제가 발생했다.

 

  • 객체 지향적인 코드 작성을 위한 방법
    • 최대한 Clean Architecture에 알맞게 MVVM 패턴을 적용하는 방법을 생각하면서 코드를 작성하도록 해야겠다. 

 

How - 리팩토링을 어떻게 시작하면 좋을지 - 개인적인 생각, 방향

 

  • 기록의 중요성
    • 코드를 작성하면서 이건 나중에 어떤식으로 수정해야지? 라는 생각이 들면 // todo 주석을 추가하여 android todo list를 통해 확인할 수 있도록 하고, 수정 내용을 코드에 담기엔 내용이 길어질 것 같은 부분은 별도로 메모하는 등 코드를 작성하면서도 틈틈이 리팩토링에 대한 고민을 해보는 것이 좋을 것 같다.

 

  • 순서 정하기
    • 0. 코드 흐름 이해 및 정리하기.
      • 어떤 식으로 변경할 수 있는지에 대한 기본적인 틀을 먼저 구상해보기.
    • 1. 테스트 케이스 준비하기
      • 항상 리팩토링 전에 코드의 기능을 확인할 수 있는 기존 작동 코드 등, 테스트 케이스를 준비하는 것이 중요할 것 같다. 원래 정상적으로 작동하던 기능이 리팩토링 후 동일하게 작동되는지 중간중간 확인해야 하기 때문.
    • 2. 분리할 수 있는 코드 먼저 분리하여 코드를 짧게짧게 나누기
      • 메서드, 객체 등 내부에 여러가지 구성요소들 중에서 재사용이 가능하거나, 구성요소의 목적, 책임을 벗어난 코드를 별도로 분리해보는 것.
    • 3. [ 2 ] 에서 나누어진 코드들을 구상한 프로그래밍 패러다임에, 디자인 패턴에 맞게 수정해보기
      • 프로젝트 초기에 객체지향적 프로그래밍 등 구상한 프로그래밍 패러다임에 맞게 작성하도록 노력해도 프로젝트를 진행하는 중간중간 지켜지지 않는 부분이 생겨날 수 있기 때문에 일관성 있도록 수정하는 것 
    • 4. 명확하지 않은 네이밍 수정하기.
      • 변수, 함수, 클래스, XML 객체 id 네이밍 등의 일관성있는 네이밍 확인하기 - 프로젝트 초기에 정해진 네이밍 룰과 일치하는 않는 부분을 수정하지 않고 진행하게 된다면 프로젝트 확장, 유지보수 시 구분이 어려워 질 수 있다.

 

 

 

 

 

 

1. ViewModel의 역할, 책임에 맞지 않는 로직 분리하기 

 

ViewModel 내부에서 Data를 변환하는 로직 별도로 Class에서 관리하도록 수정

 

수정 전 코드, 수정 전 위치

 class MyPostFeedViewModel : ViewModel() {

    ...
    
    // Post 형식의 데이터 PostRcv 형식으로 변환
    fun postToPostRcv(post: Post, uriList: MutableList<Uri>): PostRcv {
        return PostRcv(
            post.uid,
            post.title,
            post.price.toString().replace(",", "").toLong(),
            post.category,
            post.address,
            post.deadline,
            post.desc,
            uriList,
            post.nickname,
            post.likeUsers,
            post.token,
            post.timestamp,
            post.state,
            post.documentId,
            post.locationLatLng,
            post.locationKeyword,
            post.endTime
        )
    }
    
    ...
}

 

 

수정 후 코드, 수정 후 위치

object DataConverter {
    // Post 형식의 데이터 PostRcv 형식으로 변환
    fun postToPostRcv(post: Post, uriList: MutableList<Uri>): PostRcv {
        return PostRcv(
            post.uid,
            post.title,
            post.price.toString().replace(",", "").toLong(),
            post.category,
            post.address,
            post.deadline,
            post.desc,
            uriList,
            post.nickname,
            post.likeUsers,
            post.token,
            post.timestamp,
            post.state,
            post.documentId,
            post.locationLatLng,
            post.locationKeyword,
            post.endTime
        )
    }
}

 

수정 내용, 수정한 이유

  • ViewModel 내부에서 Post 타입에서 PostRcv 타입으로 변환하는 데이터 변환 로직을 사용했었는데, 이는 ViewModel의 역할이 아닌 다른 책임에 해당하여서 별도로 Class or Object 를 생성해서 사용할 수 있도록 하였다.

Class, Object?

  • Object는 전역적으로 접근이 가능하여 별도로 인스턴스화 하지 않아도 다른 클래스에서 바로 사용할 수 있다. 하지만 Object는 추가적인 상속이나 확장이 불가해서 해당 데이터 변환 로직이 추가적인 확장이 필요할 경우에는 Class를 통해 관리하는 것이 바람직하다.
  • 변환 로직의 경우, 상태를 가지지 않고 입력된 값에 따라 일관된 결과를 반환하기 때문에 객체의 인스턴스화나 상태 관리가 필요하지 않다. 이와 같은 이유로 Object를 사용.
  • 기능이 정적인 유틸리티 역할을 하는 경우 Object, 동적인 상태를 갖는 유티리티 역할을 할 경우 Class 로 만들면 될 것 같다.

 

 

 

 

2. ViewModel에서 데이터 요청 관련 로직 분리하기

 

Repository - 원격 DB 데이터 소스와, 로컬 DB 데이터 소스와의 상호작용을 추상화 하는 것으로 기존에 ViewModel에서 Firebase 원격 DB 데이터를 호출하고 가져온 데이터를 가공하는 역할까지 했으므로 ViewModel은 데이터를 가져오는 로직에 대한 방법에 상관 없이 단순하게 Repository를 통해 필요한 데이터를 요청할 수 있고 이는 UI와 데이터 소스 간의 의존성을 분리시키고, 유지보수성과 테스트 용이성을 높일 수 있다.

interface PostRepository {
    suspend fun downloadPost(collection: String): Task<QuerySnapshot>

    suspend fun uploadPost(collection: String): String

    suspend fun getUploadPost(collection: String, documentId: String): String

    suspend fun updatePost(collection: String, documentId: String): String

    suspend fun deletePost(collection: String, documentId: String): String
}

 

class PostRepositoryImpl(
) : PostRepository {

    private val db: FirebaseFirestore = Firebase.firestore
    private val storage: FirebaseStorage = Firebase.storage

    override suspend fun downloadPost(collection: String): Task<QuerySnapshot> {
        return db.collection(collection).orderBy("timestamp", Query.Direction.DESCENDING)
            .get()
    }

    override suspend fun uploadPost(collection: String): String {
        TODO("Not yet implemented")
    }

    override suspend fun getUploadPost(collection: String, documentId: String): String {
        TODO("Not yet implemented")
    }

    override suspend fun updatePost(collection: String, documentId: String): String {
        TODO("Not yet implemented")
    }

    override suspend fun deletePost(collection: String, documentId: String): String {
        TODO("Not yet implemented")
    }
}