* 적용기: 실제 개발에 적용하면서 배우게 된 내용 정리
* 현재 취준생으로 풋내기 개발자가 쓰는 글입니다.
* 그러니 조언과 지적 및 훈수는 언제나 환영입니다! 댓글로 많이 달아주세요!
부스트캠프에서 팀 프로젝트를 하기 전 과제에서,
클린 아키텍처를 적용해 본 적이 있다.
그리고 그 후 있던 프로젝트에서 클린 아키텍처를 적용한 일은 없었는데...
어떻게 적용했는지, 실제로 적용하면서 느낀 점을 짧게 풀어볼까 한다.
※ 주의 : 규모가 작은 프로젝트에서 단순하게 적용했기 때문에, 코드를 참고하기에는 미흡할 수 있습니다. 단순 고찰의 시점으로 봐주시면.. 감사합니다...
적용한 이유는?
당시 왜 적용했는가를 돌이켜 보면 딱 두가지인데
배우는 입장이니 여러가지를 해보는 게 좋겠지 + 일단 좋으니까 유명한 거 아닐까? 였던 것 같다.(부끄럽지만 사실이다)
일단 학습하는 중이니까 적용하면서 클린 아키텍처를 배우자는 생각,
그리고 이름부터 "클린"이니 코드가 깔끔하지 않을까? 하는 생각으로 적용했다.
평소에도 패키지 구조나 패턴 등 코드를 멋있고 깔끔하게 만드는 걸 좋아하기도 해서
여러 블로그를 돌아다니면서 해당 아키텍처의 적용 방법만 살펴보고 다음처럼 구현하게 됐다.
(!비공개 과제이므로 실제 코드 공개는 어려워서 변형했습니다)
만약 내가 구현하고자 하는 앱이 Todo 앱이고,
상세 페이지인 DetailActivity에서 TODO를 불러오거나(get), 삭제(delete)하고 싶다고 가정하자.
그랬을 때 대략적인 구조는 다음과 같이 설계된다.
그리고 layer의 이름을 따서 ui, domain, data 패키지를 만들었고,
패키지 내에 파일을 다음처럼 구성했다.
추가로, 편의상 같은 패키지 안에 있는 클래스는 모아서 코드에 넣었다.
// ui package
// ui - ui 로직, viewModel을 멤버 변수로 가지고 있음
class DetailActivity : AppCompatActivity() {
private var viewModel : DetailViewModel by viewModels()
// ui 로직
// ...
}
// viewModel - mvvm의 viewmodel 역할, usecase에 의존
class DetailViewModel(private val getTodoUseCase: GetTodoUseCase,
private val deleteTodoUseCase: DeleteTodoUseCase) : ViewModel() {
// usecase 호출 및 데이터 업데이트
// ...
}
// domain package
// UseCase - Repository 함수 호출, 인터페이스에 의존하도록 구성(의존성 역전 법칙)
class GetTodoUseCase(private val todoRepository: TodoRepository) {
// Repository의 함수를 호출해서 todo 가져오기
// ...
}
class DeleteTodoUseCase(private val todoRepository: TodoRepository) {
// Repository의 함수를 호출해서 todo 제거
// ...
}
// Repository 인터페이스 - 같은 domain에 두어 domain layer가 다른 layer를 참조하지 않도록 구성
interface TodoRepostory {
// todo 가져오는 함수 선언
// ...
// todo 제거하는 함수 선언
// ...
}
// data package
// Repository 구현체 : 각 DataSource의 인터페이스에 의존(의존성 역전 법칙)
class DefaultTodoRepository(
private val todoLocalDataSource: TodoLocalDataSource,
private val todoRemoteDataSource: TodoRemoteDataSource
) : TodoRepository {
// datasource로부터 todo 정보 가져오는 함수 정의
// ...
// datasource에서 todo 정보 제거하는 함수 정의
// ...
}
// LocalDataSource 인터페이스
interface TodoLocalDataSource
// RemoteDataSource 인터페이스
interface TodoRemoteDataSource
// LocalDataSource 구현체
class DefaultTodoLocalDataSource(context: Context): TodoLocalDataSource
// RemoteDataSource 구현체 : retrofit-service에 의존
class DefaultTodoRemoteDataSource(private val todoService: TodoService) : TodoRemoteDataSource
정말 좋았을까?
PR 후 리뷰어님께 의존 관계 등 구조 자체는 좋은데,
UseCase를 도입한 후 큰 이점이 보이지 않는다는 리뷰를 받았다.
(나는 그동안 멋대로 "최신 기술 == 도입하면 무조건 좋음"이라고 생각했어서 약간 충격이기도 했다.)
곰곰히 생각해보면 실제로 그랬다.
Repository는 단 하나였으며 필요한 비즈니스 로직은 모두 Repository의 함수 안에 포함시켜도 되었다.
그래서 사실 실제 UseCase 코드는 대부분 다음처럼 되었다.
class GetTodoUseCase(private val todoRepository: TodoRepository) {
// Repository의 함수를 호출해서 todo 가져오기
suspend operator fun invoke(): Result<Todo> {
return todoRepository.getTodo()
}
}
단순히 Repository의 함수를 호출한 것으로 그쳐서,
사실상 있으나 마나였고 중간 계층만 깊어진 듯 보였다.
그나마 생각해본 장점은 ViewModel이 필요한 함수가 뭔지 한눈에 보인다는 것이다.
class DetailViewModel(private val getTodoUseCase: GetTodoUseCase,
private val deleteTodoUseCase: DeleteTodoUseCase) : ViewModel()
생성자에 UseCase를 넣어줌으로서 해당 ViewModel이 필요한 함수가 뭔지 한 눈에 보인다.
만약 어떤 UseCase가 여러 곳의 ViewModel에 사용된다면 더 좋아질 거라고 예상한다.
그러나 그렇게까지 ViewModel이 많지도 않은 작은 규모의 프로젝트였으며,
프로젝트의 초반부에 적용했기 때문에 .............................(더 이상 말은 생략)
그럼 언제 적용할거야?
가능하면 다음과 같은 상황이면서,
프로젝트 규모가 어느 정도 결정된 후반부에 리팩토링할 때 검토하는 걸 선호하게 됐다.
1) Repository가 두 개 이상인 상황일 때 두 개 이상의 Repository를 같이 호출해 새로운 데이터를 반환해야 할 때
2) 여러 ViewModel에서 똑같은 비즈니스 로직이 사용될 때
예를 들면, 어떤 상품을 구매하는 함수를 호출하되, 사용자에 따라(ex. VIP 고객 등) 파라미터가 달라진다고 가정하자.
그런데 사용자 정보를 호출하는 함수는 UserRepository에 있고,
구매하는 함수는 StoreRepository가 있다고 해보자.
ViewModel이 Repository 두 개를 가지고 있고, 조합하는 걸 ViewModel에서 하게 되는 상황이 일어난다.
데이터를 조합하는 해당 로직이 만약 해당 Viewmodel 뿐만이 아니라
다른 ViewModel에서도 재사용된다면 어떻게 될까? 같은 코드를 Ctrl + C, V 하게 되는 상황이 벌어진다.
이렇게 Repository가 비즈니스 로직을 넣지 못하는 상황일 때
UseCase를 도입한다면 이점을 크게 누릴 거라고 생각한다.
+ 후반부 리팩토링할 때 검토하게 되는 이유는, 안드로이드를 오래 개발했다면 모르지만 아직 초짜인 입장에서 이런 상황을 설계 때 예측하기는 어렵다고 생각해서.
공식 문서의 도메인 계층
도메인 계층에 관한 공식 문서 글이 존재하며, 실제로 이렇게 적혀있다.
모든 앱에 이러한 요구 사항이 있는 것은 아니므로 이 계층은 선택 사항입니다. 예를 들어 복잡성을 처리하거나 재사용성을 선호하는 경우와 같이 필요한 경우에만 사용해야 합니다.
도메인 계층의 장단점, 규칙, 종속성에 관한 내용도 있고
내가 실제로 적용하면서 느낀 장점(가독성 향상)에
Repository 결합에 관한 내용도 있다.(혼자 고민하고 내린 결론이었는데 공식 문서에 있어서 매우 놀랐따...)
마치며
이 때 리뷰어님께 이런 저런 질문을 듣고 나서 전과 달라진 게 있는데,
설계 시에 해당 기술이 과연 내 프로젝트에 도움이 되는가?에 대한 고민을 하게 되었다.
최신 기술이든, 패턴이든 뭐든 당연히 좋겠지라는 생각을 버리게 됐다.
프로젝트 규모나 인원, 구조에 따라서는 해당 기술은 득이 될 수도, 실이 될 수도 있기 때문에
그런 기술을 적용할 때에는 신중해지고, 깊이 고민해보는 시간을 가졌다.
(끝나고 나서 경험을 정리하는 것도 중요하게 여기고 있다.)
그리고 이것은 그룹 프로젝트에서 멀티 모듈 적용기로 이어진다...(다음에 계속)
'Android' 카테고리의 다른 글
[TIL/개념] Hilt로 의존성 주입하기: 개념 + 겪은 이슈 정리 (6) | 2023.07.12 |
---|---|
[TIL/개념] Android Notification + PendingIntent (0) | 2023.07.11 |
[적용기] MVVM ViewModel과 AAC ViewModel (4) | 2023.05.10 |
[TIL/개념] Android의 Context와 ApplicationContext (0) | 2023.04.30 |
[TIL/개념] Activity와 Fragment (2) - Fragment와 Fragment의 lifecycle (0) | 2023.03.21 |