커디(Kerdy) 프로젝트를 하며 UI / Data 패키지 구조 변경 논의 이후에 작성하였습니다.
패키지 구조에는 크게 2가지
가 존재한다.
계층형 구조
는 마치 레이어가 존재하는 것처럼 계층적으로 패키지를 묶는 구조
이다.
아래와 같은 구조가 계층형 구조에 해당한다.
- repository
- service
- dto
+ response
+ request
- mapper
Repository, Service 등이 레이어를 이루는 것처럼 표현할 수 있다.
Depth
가 깊지 않다.Service는 어디에 있을까?
와 같은 생각이 필요 없다.모델의 패키지 관련성
을 깊게 고민하지 않아도 된다.여러 패키지
를 찾아봐야 한다.Repository
와 Service
를 찾기 위해 두 개의 패키지를 열어야 한다.도메인형 구조
는 도메인 모델과 관련된 파일들을 하나로 모으는 구조
이다.
(커디에서는 도메인 대신 데이터 모델을 사용합니다.)
아래와 같은 구조가 도메인형 구조에 해당한다.
- member
+ repository
+ mapper
+ dto
* response
* request
+ service
+ model
- ...
모델에 대한 높은 응집도
를 가진 패키지 구조를 형성할 수 있다.Depth
가 깊다.Conference
와 Competition
이 모두 EventStatus
를 사용한다고 했을 때, EventStatus는 어느 패키지에 존재해야 하는가?커디 프로젝트는 기존에 다음과 같은 구조를 따랐다.
- Data : 도메인형 구조
- UI : 계층형 구조
기존 구조로 패키지를 구성하면 실제 안드로이드 프로젝트에서는 이렇게 나타낼 수 있다.
관련성 있는 화면
끼리 묶기 위해, 계층형 구조
를 따르던 UI 패키지
는 사진으로만 봐도 난독증이 올 정도로 복잡하다.
(완전한 계층형 구조는 아니지만, 도메인형 보다는 계층형 구조에 가깝다는 이유로 편의상 계층형 구조라고 표현하였습니다.)
대회 필터 화면
하나를 찾기 위해 몇 Depth 를 파고 들어간지 모르겠다.
(main
of event
of competitionFilter
)
도메인형 구조
를 따르는 Data 패키지
이다.
data 패키지를 열면 그 하위에는 데이터 모델 패키지들이 수십개
가 보인다.
열자마자 뒤로가기를 누르고 싶은 구조이다.
또한, 사진에서 보이는 것처럼 ActivityType 모델이 다른 곳에서도 쓰인다면 어떤 패키지에 존재해야 하는가
매번 고민해야 한다.
이러한 구조에 실증을 느낀 나머지 커디(Kerdy) 패키지 구조 개선 디스커션 을 통해 의견을 공유하였다.
팀원 모두 동일한 감정을 느꼈고, 이에 따라 과감히 프로젝트 구조 개선에 나섰다.
정리하자면 다음과 같다.
UI
는 특정 화면을 즉시 찾는 것이 편하기 때문에 도메인형 구조
로 변경하자.Data
는 데이터 모델간의 관련성을 결정하기 모호하고, 소규모 프로젝트이므로 (한 계층) 패키지에 파일을 모아두어도 복잡하지 않으므로 계층형 구조
로 변경하자.리팩터링을 진행한 이후의 패키지 구조는 다음과 같다.
UI 패키지 구조 Data 패키지 구조
UI
패키지는 도메인형 구조
로 변경하여 특정 화면을 한 번에 찾기 쉬운 구조가 되었다.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
// Before
class FooCreateRequestBody(...)
// After
class FooCreateRequest(...)
// 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
CREATE
: XxxCreateRequestREAD
: XxxGetRequestUPDATE
: XxxUpdateRequestDELETE
: 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>>
}
2 Detph
까지 허용하는 구조로 변경하였습니다.2 Depth
를 허용합니다.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
를 붙여 명명하도록 변경하였습니다.ex) events 패키지
-> eventList 패키지
xxxPageList
로 명명하도록 변경하였습니다.커디는 다음 과정을 거치며 패키지 구조를 개선하였습니다.