날씨 API 요청 응답에 ‘캡슐화-위임 숨기기’ 리팩토링 적용하기
마틴 파울러의 ‘리팩토링’ 스터디 내용 실제 프로젝트에 적용하기
딜라이트룸에서는 매주 정기적으로 앱의 코드 품질을 높이고자 개발자 스터디를 진행한다. 딜라이트룸에는 Android, iOS, Data & Infra 개발자들이 있고 이들이 사용하는 언어는 각기 다르기 때문에 항상 스터디 주제로 각 언어나 개발 플랫폼에 독립적인 주제를 학습 하고자 한다.
최근에는 코드 품질을 꾸준히 좋게 유지하기 위해 마틴 파울러 형님이 지으신 ‘리팩터링 2판’ 을 학습 중이다. 스터디에서 학습한 내용을 실제 프로젝트에 적용하고자 고민하던 중 최근 기존 날씨 API 를 새로운 날씨 API 사용으로 변경하게 될 일이 있어 해당 작업에 학습한 내용 중 캡슐화 파트의 ‘위임 숨기기’ 리팩토링을 적용한 경험을 공유하고자 한다.
현재 알라미 앱에서는 ‘오늘의 패널’ 탭에서 다음과 같은 형태로 날씨 정보를 제공하고 있고 해당 기능을 위해 Accuweather 에서 제공하는 날씨 정보 API 를 사용중에 있다. 이번에 오늘의 패널 개편 작업 중 날씨 API 를 Accuweather API 에서 Openweather API 로 변경하게 될 일이 생겼고 해당 변경 작업을 진행하며 개발 스터디에서 학습한 캡슐화 리팩터링 기법 중 ‘위임 숨기기’ 를 적용해보고자 했다.
{
"value": {
"geo": { ... },
"headline": { ... },
"current": { ... },
"airquality": { ... },
"hourly_forecast": [ ... ],
"daily_forecast": [{
"date_time_ms": 1628175600000,
"temperature": {
"min": {
"value": 10,
"unit": "C"
},
"max": {
"value": 35,
"unit": "C"
}
},
...
},
...
]
}
}
API 요청 응답 중 주간 예보를 표기하기 위한 정보를 받아오는 예제를 준비해보았다. 날씨 정보 요청에 대해 다음 예시 형태로 JSON 응답을 받아온다. 이와 같은 계층적 Key-Value 형태의 JSON 포맷의 데이터를 처리하기 위해 Kotlin 에서 각각의 JsonObject 에 대응되는 계층적으로 구성된 다음 형태의 Data Class 들이 필요하다. 리팩토링 적용 전 후 비교를 위해 캡슐화를 적용하기 전의 Data Class 형태로 구성했다.
data class Weather(
val geo: WeatherGeo,
val headline: WeatherHeadline,
...
@SerailizedName("daily_forecast") val dailyForecasts: List<DailyForecast>,
)
data class DailyForecast(
...
val temperature: MinMaxTemperature,
...
)
data class MinMaxTemperature(
val max: Temperature,
val min: Temperature,
)
data class Temperature(
val value: Int,
val unit: String,
)
예시 화면의 주간 예보 정보 중 ‘최고/최저 기온’ 정보를 표기하기 위해서 ‘weather.dailyForecasts.get(n).temperature.max.value
’와 같은 체이닝 형태의 객체 접근이 필요하다. 이와 같은 연쇄 의존성은 체이닝을 구성하는 객체 중 어느 한 객체의 속성 및 기능에 변경 사항이 생기면 해당 객체에 의존하는 객체에도 연쇄적으로 변경이 필요하게 되기 때문에 결합도가 높은, 유지보수에 좋지 않은 코드이다. 연속적인 객체 접근으로 코드가 길어져 해당 코드를 이해하기 위해 각 객체를 이해해야 하기 때문에 가독성도 해치고 있다. 이와 같은 케이스는 캡슐화가 되어 있지 않아, 모듈성이 낮아 발생하는 문제라고 볼 수 있다. 예제 코드의 DailyForecast 클래스에 위임 메서드를 만들고 ‘위임 숨기기’ 를 통한 캡슐화를 적용하여 위임 객체의 존재를 숨겨 보고 이러한 리팩토링이 어떤 효과를 가져오는지 살펴보겠다.
data class Weather(
val geo: WeatherGeo,
val headline: WeatherHeadline,
...
val dailyForecasts: List<DailyForecast>,
)
data class DailyForecast(
...
private val temperature: MinMaxTemperature,
...
) {
val minTemperature = temperature.min.value
val maxTemperature = temperature.max.value
}
data class MinMaxTemperature(
val max: Temperature,
val min: Temperature,
)
data class Temperature(
val value: Int,
val unit: String,
)
DailyForecast 객체의 temperature 속성을 private 접근제어자와 함께 선언하여 외부로부터 감추고 minTemperature, maxTemperature 위임 메서드를 선언하여 최고, 최저 기온 정보에 접근 가능하도록 만들었다. 이와 같은 형태로 '위임 숨기기' 를 적용하면 예제 화면의 '최고/최저 기온' 정보를 표기하기 위한 'weather.dailyForecasts.get(n).temperature.max.value
' 코드를 'weather.dailyForecasts.get(n).maxTemperature
' 와 같이 개선할 수 있다. 실제로는 반복문을 통해 각 DailyForecast 객체에 접근하기 때문에 다음 형태로 구성될 것이다.
// 캡슐화 '위임 숨기기' 적용 전
weather.dailyForecasts.forEach { forecast ->
forecast.temperature.max.value
}
// 캡슐화 '위임 숨기기' 적용 후
weather.dailyForecasts.forEach { forecast ->
forecast.maxTemperature
}
최고 기온 정보에 접근하기 위한 'forecast.temperature.max.value
' 형태의 접근이 'forecast.maxTemperature
' 형태로 변경 되었다. temperature 속성을 캡슐화 하여 불필요한 객체로의 접근을 방지하고 maxTemperature 위임 메서드를 선언하여 DailyForecast 내부 구현을 감췄다. 캡슐화를 통해maxTemperature 를 사용하는 위치에서는 Temperature 내부의 변경에는 영향을 받지 않게 함으로써 불필요한 변경을 방지할 수 있다. 또한 최고 기온을 의미하는 'maxTemeprature' 메서드 명으로 가독성을 높일 수 있다.
이처럼 날씨 API 변경 기회를 활용해 스터디에서 학습한 리팩터링 기법을 적용해 볼 수 있었다. 하지만 책에서도 안내되어 있듯 리팩터링은 프로그래밍 과정에서 If 문 작성과 같이 특별히 큰 작업으로 기회를 만들어 하는 것이 아닌 평소 개발 과정에서 녹여내야 한다. 앞으로도 꾸준한 스터디를 통해 실제 프로젝트에 적용한 좋은 예시가 있다면 종종 소개해 보려한다.
딜라이트룸에서 값진 경험을 함께 하실 분들을 모십니다