상위 클래스를 두어 해결한 문제점에 대해 제시했던 팀원의 의견과 나의 생각을 적어보고자 한다.
새롭게 합류한 팀의 코드를 보다가 RemoteDataSource
에서 아래와 같은 보일러플레이트 코드를 발견했다.
override suspend fun getUser(): Result<User> = try {
service.getUser().toResult()
} catch (e: IOException) {
...
)
}
RemoteDataSource
내 모든 함수들이 try-catch
문으로 래핑되어있었고, 그 양은 어마어마했다.
마침 내게도 개발할 새로운 피쳐가 생겼기에 RemoteDataSource
를 건드려야 했고, 이를 개선하고자 했다.
위와 같은 방식으로 개발을 진행하게 된다면 보일러 플레이트 코드로 인해 코드의 양이 많아지고
관리가 힘들어지고 매번 try-catch
를 써야한다는 것은 굉장히 귀찮은 일이다.
또한, RemoteDataSource
클래스를 파악할 때 쓸데없는(?) 코드가 많기 때문에 가독성이 저하된다.
물론 try-catch
자체가 쓸데없다는 것은 아니고, 예외 처리를 하는 것을 굳이 RemoteDataSource
클래스가 알고있어야 하냐는 것이다.
RemoteDataSource
의 책임은 API 호출을 담당하는 것이므로 예외 처리를 해도 RemoteDataSource
클래스에서 해도 되지 않냐라는 질문에는 맞다라는 대답을 할 것이다.
하지만 굳이? 라는 생각이 1000번도 넘게 들 것 같다. 예외처리에 대한 역할은 다른 클래스에게 위임하면 되는 것 아닌가..?
마침 개발해야 할 피쳐가 새로운 API를 사용해야 했기에 RemoteDataSource
를 건드려야 하는 상황이었다.
그래서 이참에 try-catch
지옥에서 벗어나기 위해 개선을 시도했다.
(특히 나는 불필요한 반복 작업을 싫어한다..)
우선 설계를 시작했다.
RemoteDataSource
는 API 호출에 대한 책임만을 갖게하고,
이를 안전하게 처리하는 책임을 가진 클래스를 하나 두면 내가 느낀 불필요한 중복의 문제점을 해결할 수 있다고 생각했다.
open class BaseRemoteDataSource @Inject constructor() {
inline fun <T> safeApiCall(apiCall: () -> Response<T>): Result<T> {
return try {
...
} catch(throwable: Throwable) {
...
}
BaseRemoteDataSource
는 이제 안전하게 API 호출을 처리하는 책임을 갖게 되었고, 각 RemoteDataSource
에서는 이를 상속받아 safeApiCall
를 사용하면 된다.
// 기존
override suspend fun getUser(): Result<User> = try {
service.getUser().toResult()
} catch (e: IOException) {
...
)
}
// 개선
override suspend fun getUser(): Result<User> = safeApiCall {
service.getUser()
}
이러한 개선안을 팀 내 공유했고 의견을 받았다. 여러 의견들이 있었다.
클래스명에 대한 의견이 있었고 다수결에 따라 결정했다.(위 예시에 나온 BaseRemoteDataSource
는 아니다.)
이외에 한 팀원이 이러한 제안을 했다. "사용하기 편하게 단일 파일 함수로 빼서 관리하는 건 어때요?"
여기서 많은 의문이 들었다. "과연 정말 사용하기 편할까?", "지금 방식대로 클래스로 관리하는 것이 무슨 장점이 있을까?" 등등
고민 끝에 해당 제안에는 반대되는 입장을 표했다.
애초에 저 클래스를 만들게 된 의도는 'API 호출에 대한 안전성 보장' 책임을 줌으로써 DataSource
에서는 호출의 책임만 가지게 한 뒤 중복 제거 및 안전성을 가져가려 했던 것이기 때문이다.
내가 생각했을 때 단일 파일 함수로 빼서 관리하면 '사용'자체에는 편리한 부분이 있을 것이다. RemoteDataSource
에서 BaseRemoteDataSource
를 상속받지 않아도 바로 함수 호출이 가능하기 때문이다.
하지만 이러한 점이 문제가 된다고 생각했다. 상속을 받지 않아도 아무곳에서나 접근이 가능하기 때문이다. 즉 책임의 경계가 불명확해진다.
이렇게 된다면 로컬 api 호출을 사용하기 위해 누군가가 호출을 했고, 당연히(?) 로컬 호출에 대한 책임은 없음에도 불구하고 잘 작동하게 하기 위해 safeApiCall
함수를 수정할 수도 있다.
이 순간 safeApiCall
을 만든 의도자체가 퇴색되어 버리는 것이고, OCP에 대한 원칙도 깨져버리는 것이다.
또한, 실제로 사용 시 receiver
를 제네릭으로 받고 있기 때문에 safeApiCall
가 필요없는 상황에서도 IDE에 의해 제안되어 개발에 불편함을 초래할 수도 있다.
그렇기에 BaseRemoteDataSource
의 구조는 그대로 유지했고, 각 RemoteDataSource
에서는 BaseRemoteDataSource
를 상속받아 safeApiCall
를 사용하여 보일러플레이트 코드를 줄이는 개선을 맛봤다.