커디(Kerdy)의 Android 패키지 구조 리팩터링

부나·2023년 9월 14일
7

안드로이드

목록 보기
3/12
post-thumbnail

커디(Kerdy) 프로젝트를 하며 UI / Data 패키지 구조 변경 논의 이후에 작성하였습니다.


패키지 구조에는 크게 2가지 가 존재한다.

  • 계층형 구조
  • 도메인형 구조

계층형 구조

계층형 구조 는 마치 레이어가 존재하는 것처럼 계층적으로 패키지를 묶는 구조 이다.
아래와 같은 구조가 계층형 구조에 해당한다.

- repository
- service
- dto
  + response
  + request
- mapper

Repository, Service 등이 레이어를 이루는 것처럼 표현할 수 있다.

장점

  • 도메인형 구조에 비해 Depth 가 깊지 않다.
  • 프로젝트 구조가 단순해서 파악하기 편하다.
    • Service는 어디에 있을까? 와 같은 생각이 필요 없다.
  • 모델의 패키지 관련성 을 깊게 고민하지 않아도 된다.

단점

  • 특정 모델에 대한 클래스를 여러개 찾아야 한다면, 여러 패키지 를 찾아봐야 한다.
    • Conference RepositoryService 를 찾기 위해 두 개의 패키지를 열어야 한다.
  • 프로젝트 규모가 커질 수록 한 패키지에 파일이 많아져서 보기 복잡하다.

도메인형 구조

도메인형 구조도메인 모델과 관련된 파일들을 하나로 모으는 구조 이다.
(커디에서는 도메인 대신 데이터 모델을 사용합니다.)
아래와 같은 구조가 도메인형 구조에 해당한다.

- member
  + repository
  + mapper
  + dto
    * response
    * request
  + service
  + model
- ...

장점

  • 모델에 대한 높은 응집도 를 가진 패키지 구조를 형성할 수 있다.
  • 관련된 모델을 한 곳에서 확인할 수 있어서 여러 패키지를 확인하지 않아도 된다.

단점

  • 계층형 구조에 비해 패키지의 Depth 가 깊다.
  • 모델의 관련성을 정의하는 기준이 모호해진다.
    • ex) ConferenceCompetition 이 모두 EventStatus 를 사용한다고 했을 때, EventStatus는 어느 패키지에 존재해야 하는가?
    • 이로 인해, 예상했던 패키지에 모델이 존재하지 않을 수도 있다.

커디(Kerdy)가 겪은 패키지 구조 문제점

커디 프로젝트는 기존에 다음과 같은 구조를 따랐다.

- Data : 도메인형 구조
- UI : 계층형 구조

기존 Kerdy의 패키지 구조

기존 구조로 패키지를 구성하면 실제 안드로이드 프로젝트에서는 이렇게 나타낼 수 있다.

관련성 있는 화면 끼리 묶기 위해, 계층형 구조 를 따르던 UI 패키지 는 사진으로만 봐도 난독증이 올 정도로 복잡하다.
(완전한 계층형 구조는 아니지만, 도메인형 보다는 계층형 구조에 가깝다는 이유로 편의상 계층형 구조라고 표현하였습니다.)

대회 필터 화면 하나를 찾기 위해 몇 Depth 를 파고 들어간지 모르겠다.
(main of event of competitionFilter)


도메인형 구조 를 따르는 Data 패키지 이다.
data 패키지를 열면 그 하위에는 데이터 모델 패키지들이 수십개 가 보인다.
열자마자 뒤로가기를 누르고 싶은 구조이다.

또한, 사진에서 보이는 것처럼 ActivityType 모델이 다른 곳에서도 쓰인다면 어떤 패키지에 존재해야 하는가 매번 고민해야 한다.


이러한 구조에 실증을 느낀 나머지 커디(Kerdy) 패키지 구조 개선 디스커션 을 통해 의견을 공유하였다.
팀원 모두 동일한 감정을 느꼈고, 이에 따라 과감히 프로젝트 구조 개선에 나섰다.

정리하자면 다음과 같다.

  • UI 는 특정 화면을 즉시 찾는 것이 편하기 때문에 도메인형 구조 로 변경하자.
  • Data 는 데이터 모델간의 관련성을 결정하기 모호하고, 소규모 프로젝트이므로 (한 계층) 패키지에 파일을 모아두어도 복잡하지 않으므로 계층형 구조 로 변경하자.

개선된 Kerdy의 패키지 구조

리팩터링을 진행한 이후의 패키지 구조는 다음과 같다.

UI 패키지 구조 Data 패키지 구조

  • UI 패키지는 도메인형 구조 로 변경하여 특정 화면을 한 번에 찾기 쉬운 구조가 되었다.
    • 화면의 이름은 파악하기 쉽도록 개선할 예정이다.
  • Data 패키지는 계층형 구조 로 변경하여 훨씬 단조로워지고, 데이터 모델간 패키지 연관성을 고려하지 않아도 된다.


Kerdy 패키지 컨벤션

아래는 커디 프로젝트에서 새롭게 리팩터링한 내용을 문서화한 내용이다.

Data 패키지

도메인형 구조에서 계층형 구조로 변경하였습니다.

  • 깊이가 깊어지고 프로젝트 규모가 커지면서 통일성이 없어지는 문제를 해결하였습니다.
// Before : 도메인형 구조
- data
  - comment
    - dto
      - CommentUpdateRequestBody.kt
      - CommentDeleteRequestBody.kt
      - CommentCreateRequestBody.kt
      ...
    - mapper
      - CommentMapper.kt
    - CommentService.kt
    - CommentRepository.kt
    - CommentRepositoryImpl.kt
  ...
  - recruitment
  - member
  ...

// After : 계층형 구조
- data
  - apiModel
    - request
      - FooRequest.kt (FooService에서 다루는 Requests)
      - BooRequest.kt
      ...
    - response
      - FooResponse.kt (FooService에서 다루는 Responses)
      - BooResponse.kt
      ...
  - model
  - service
  - dataSource
    - remote
    - local

Request/Response API 클래스 끝에 Body suffix를 제거하였습니다.

// Before
class FooCreateRequestBody(...)

// After
class FooCreateRequest(...)

관련된 Request/Response끼리 한 파일에서 관리하도록 변경하였습니다.

// Before
class FooCreateRequest.kt
class FooDeleteRequest.kt
class FooUpdateRequest.kt

// After
FooRequest.kt
- data class FooCreateRequest(...) // in FooRequest.kt
- data class FooDeleteRequest(...) // in FooRequest.kt
- data class FooUpdateRequest(...) // in FooRequest.kt

CRUD에 따라 Request 클래스에 이름을 붙이도록 통일하였습니다.

  • CREATE : XxxCreateRequest
  • READ : XxxGetRequest
  • UPDATE : XxxUpdateRequest
  • DELETE : XxxDeleteRequest

통일성을 위해 호스트 뒤에 이어지는 경로가 /로 시작하도록 변경하였습니다.

// Before
@POST("reports")
suspend fun reportComment(...)

// After
@POST("/reports")
suspend fun reportComment(...)

가독성을 높이기 위해 요청에 필요한 파라미터를 한 줄에 표현하지 않고, 한 줄씩 띄도록 변경하였습니다.

// Before
@DELETE("/comments/{commentId}")
suspend fun deleteComment(@Path("commentId") commentId: Long): Response<Unit>

// After
@DELETE("/comments/{commentId}")
suspend fun deleteComment(
    @Path("commentId") commentId: Long,
): Response<Unit>

분업을 하면서 발생한 여러개의 중복 Service, Repository를 하나로 통합하였습니다.

e.g.EventServiceDetail.kt의 메서드를 EventService.kt로 통합

// Before
// EventServiceDetail.kt
interface EventDetailService {
    @GET("/events/{eventId}")
    suspend fun getEventDetail(
        @Path("eventId") eventId: Long,
    ): ApiResponse<EventDetailResponse>
}

// After (EventDetailService에 있던 메서드를 EventService로 이동)
// EventService.kt
interface EventService {
    @GET("/events/{eventId}")
    suspend fun getEventDetail(
        @Path("eventId") eventId: Long,
    ): ApiResponse<EventDetailResponse>
   
    ...

    @GET("/events")
    suspend fun getConferences(
        @Query("category") category: String,
        @Query("statuses") statuses: List<String> = emptyList(),
        @Query("tags") tags: List<String> = emptyList(),
        @Query("start_date") startDate: String? = null,
        @Query("end_date") endDate: String? = null,
    ): Response<List<ConferenceResponse>> 
}


Presentation 패키지

깊은 Depth를 피하고자 최대 2 Detph까지 허용하는 구조로 변경하였습니다.

  • 부모의 ViewModel을 공유하는 자식 화면만 2 Depth를 허용합니다.
    • ex) OnboardingActivity의 OnboardingJobFragment, OnboardingNameFragment, OnboardingClubFragment
  • 그 외에는 1 Depth를 유지합니다.
  • 깊이가 깊어지면서 프로젝트를 파악하기 어려운 문제를 해결하였습니다.
// Before : 관련 있는 화면을 패키지 안에 중첩해서 추가하다보니, 원하는 화면 클래스를 찾기 어려운 문제 발생!
- ui
  - main (1 depth)
    + eventList (2 depth)
      * conference (3 depth) 😡😡😡
        - conferenceListActivity.kt
        - conferenceListViewModel.kt
      * conferenceFilter
        - conferenceFilterActivity.kt
        - conferenceFilterViewModel.kt
      * competition
        - ...
      * competitionFilter
        - ...
      ...
    - 

// After
- ui
  - main
  - profile
  - eventList
  - setting
  - conferenceFilter
  - onboarding (1 depth)
    + NameOnboarding (2 depth) 👍👍 // OnboardingActivity와 ViewModel 공유
    + JobOnboarding (2 depth) 👍👍 // OnboardingActivity와 ViewModel 공유
    + ActivityOnboarding (2 depth) 👍👍 // OnboardingActivity와 ViewModel 
    + OnboardingActivity.kt
    + OnboardingViewModel.kt공유
  ...


목록을 보여주는 화면은 s(영어의 복수형) 대신에 List를 붙여 명명하도록 변경하였습니다.

  • 패키지 글자 크기가 작아 s까지 자세히 보지 않고도 파악할 수 있습니다.

ex) events 패키지 -> eventList 패키지

여러 화면을 담기만 하는 화면의 이름은 xxxPageList로 명명하도록 변경하였습니다.

  • 더 나은 이름이 떠오르면 추후에 변경하겠습니다.

Reference

커디는 다음 과정을 거치며 패키지 구조를 개선하였습니다.

  1. 커디(Kerdy) 패키지 구조 개선 디스커션
  2. 커디(Kerdy) 패키지 구조 개선 이슈
  3. 커디(Kerdy) 패키지 구조 개선 PR
profile
망각을 두려워하는 안드로이드 개발자입니다 🧤

0개의 댓글