4년 전 코드의 문제점
'마스크 알라미' 앱에서 제일 중요한 로직은 아래와 같다.
"판매처(Store) 데이터를 외부에서 제공받아 재고량, 위경도 값을 추출해 마커로 만든 뒤 지도에 렌더링한다"
이를 위해 앱에서 해줘야 할 일은 다음과 같다.
- 서버(외부 리소스)에 데이터 요청하기
- 요청이 성공했는지 실패했는지 구분
- 데이터를 응답받으면 필요한 값을 추출해 마커로 만들기
- 생성된 마커들을 맵 UI에 렌더링하기
앱에서 이러한 행위들을 하기 위해 나는 어떻게 구조를 설계하고 코드를 작성했을까?
MainActivity
public class MainActivity extends BaseActivity implements MainActivityView {
private ArrayList < Marker > mMarkers = null;
private void getStoresByGeo(double lat, double lng, int m) {
showProgressDialog();
MainService mainService = new MainService(this);
mainService.getStoresByGeo(lat, lng, m);
}
@Override
public void getStoresByGeoSuccess(int count, ArrayList<Store> stores) {
if (mNaverMap != null) {
setMarkers(stores);
}
}
@Override
public void getStoresByGeoFailure(String message) {
hideProgressDialog();
}
private void setMarkers(ArrayList<Store> stores) {
for (final Store store : stores) {
if (store.getRemain_stat() != null) {
final Marker marker = new Marker ();
marker.setPosition(new LatLng (store.getLat(), store.getLng()));
marker.setMap(mNaverMap);
mMarkers.add(marker);
}
}
}
}
MainActivityView
public interface MainActivityView {
void getStoresByGeoSuccess(int count, ArrayList<Store> stores);
void getStoresByGeoFailure(String message);
}
MainService
class MainService {
private final MainActivityView mMainActivityView;
MainService(final MainActivityView mainActivityView){
this.mMainActivityView = mainActivityView;
}
void getStoresByGeo(double lat, double lng, int m) {
final MainRetrofitInterface mainRetrofitInterface =
getPublicMaskRetrofit().create(MainRetrofitInterface.class);
mainRetrofitInterface.getStoresByGeo(lat, lng, m).enqueue(new Callback < MainResponse >() {
@Override
public void onResponse(Call<MainResponse> call, Response<MainResponse> response) {
final MainResponse mainResponse = response.body();
if (mainResponse == null) {
mMainActivityView.getStoresByGeoFailure(null);
return;
} else {
mMainActivityView.getStoresByGeoSuccess(
mainResponse.getCount(),
mainResponse.getStores()
);
}
}
@Override
public void onFailure(Call<MainResponse> call, Throwable t) {
mMainActivityView.getStoresByGeoFailure("onResponse failed");
}
});
}
}
(Store 데이터를 불러오고 렌더링 해주는 과정 외에 불필요한 부분은 생략하고
4년 전 코드는 Java로 작성했었다)
구조를 간략하게 보면 서버와 네트워크 호출을 하는 로직을 MainService 객체가 담당하고, MainActivityView 인터페이스로 MainService와 MainActivity 사이를 연결해 성공, 실패를 MainActivity에서 처리한다.
이같은 구조의 가장 큰 문제점은 UI 컨트롤러인 MainActivity 안에 UI 로직과 비즈니스 로직이 함께 있다.
설명을 위해 위의 코드에서는 많은 부분을 생략해놓았지만 실제로 MainActivity는 800줄이 넘는 상태이다.
이는 비즈니스 로직이 UI 로직에 영향을 줄 수 있고 무엇보다 가독성이 안좋아 기능 추가나 수정을 어렵게 만든다.
또한, 데이터의 흐름을 파악하기 어려워 디버깅도 힘들다.
UI 컨트롤러는 최대한 단순화하기 위해 비즈니스 로직을 분리하고
이를 담당하는 중간 계층인 ViewModel를 추가한다.
MainService 역시 MainActivity 와 인터페이스를 통해 직접적으로 연결되어 있다.
이는 비즈니스 로직이 UI 에 강하게 결합되어 있으면 유지보수, 확장성 측면에서 좋지 않다.
데이터 레이어와 UI 레이어를 분명하게 나누고 Service가 하던 일을 DataSource에게 넘기려고 한다.
UI 레이어는 데이터를 받아 로직을 처리하거나 UI 변경사항을 반영하기만 하면 된다.
또한, Repository 패턴을 도입해 UI 레이어가 데이터를 로컬이나 원격 어디에서 받는지 몰라도 비즈니스 로직을 수행할 수 있게 한다.
레이어를 나누고 역할을 분명히 함으로써 각자가 본인의 책임만을 성실히 수행할 수 있게 만들어준다.
클린아키텍처를 준수하며 MVVM 패턴과 Repository패턴을 적용해서 리팩토링 해보려고 한다.
앱의 핵심 데이터인 판매처(Store) 데이터의 흐름에 초점을 맞춰 리팩토링 과정을 설명하려 한다.
앱 아키텍처 재설계
...
├─ mask_alarmi
│ ├─ main
│ ├─ interfaces
│ ├─ MainRtorfitInterface.java
│ └─ MainActivityView.java
│ ├─ models
│ └─ Store.java
│ ├─ MainActivity.java
│ └─ MainService.java
│ ├─ splash
...
리팩토링하기 전 디렉토리 구조를 보면 화면별로 Service와 Activity 중심으로 모듈이 구성되어있다.
이 같은 방식도 Service가 네트워크 호출을 담당하니 데이터 레이어와 UI 레이어가 구분되었다고 할 수 있다.
하지만 화면이 많아질수록 Service와 Activity에서 중복되는 코드가 많아질 것이다.
그리고 Activity에서 비동기 네트워크 호출을 요청하고 있기 때문에 UI 로직 처리에 방해가 될 수 있다.
따라서 데이터 레이어와 UI 레이어를 분명히 구분 짓고,
데이터 레이어는 외부 리소스의 데이터를 불러와 UI 레이어에 전달하는 책임을 잘 수행하도록 설계한다.
UI 레이어는 UI 로직과 비즈니스 로직을 잘 수행할 수 있도록 UI 컨트롤러와 프레젠터의 역할을 구분해준다.
아래는 변경된 아키텍처의 디렉토리 구조이다.
...
├─ mask_alarmi
│ ├─ data
│ ├─ local
│ ├─ entities
│ └─StoreEntitiy
│ ├─ StoreDao
│ └─ StoreLocalDataSource
│ ├─ repository
│ ├─ MaskAlarmiRepository
│ └─ MaskAlarmiRepositoryImpl
│ └─ models
│ └─ Store.java
│ ├─ ui
│ └─ main
│ ├─ map
│ ├─ MapFragment
│ └─ MapViewModel
│ └─ MainActivity
...
디렉토리 구조를 ui 패키지와 data 패키지로 나눔으로써 레이어를 추상화하였다.
모바일 클린 아키텍처의 domain 계층은 굳이 필요할 것 같이 않아 두 레이어로만 구성했다.
MapFragment 화면이 생성될 때 MapViewModel 에서 레포지토리에 판매처 정보를 요청한다.
MaskAlarmiRepository는 DataSource에 정보를 요청하고 전달받아 MapViewModel 에게 전달한다.
MapViewModel이 받은 데이터로 MapFragment에서 StoreMarker 리스트를 생성한다.
마지막으로 생성된 StoreMarker들이 지도 화면에 렌더링된다.
(공적 마스크 오픈 API 사용이 불가해져 Repository를 통해 원격 서버에서 데이터가 오는 것처럼
구현하기 위해 더미데이터를 기기 내부 DB에 저장해놓고 Room을 활용해 사용하였다.
원격 API를 이용한다면 LocalDataSource가 아닌 RemoteDataSource로만 변경해주면 된다.)
'프로젝트 > 마스크 알라미' 카테고리의 다른 글
4년 전 코드 리팩토링하기 (4) - Repository 패턴 (0) | 2023.10.29 |
---|---|
4년 전 코드 리팩토링하기 (3) - MVVM 패턴 (0) | 2023.10.29 |
4년 전 코드 리팩토링하기 (1) (0) | 2023.10.20 |