안녕하세요. 커뮤니케이션 앱 LINE의 모바일 클라이언트를 개 발하고 있는 Ishikawa입니다.
저희 회사는 높은 개발 생산성을 유지하기 위해 코드 품질 및 개발 문화 개선에 힘쓰고 있습니다. 이를 위해 다양한 노력을 하고 있는데요. 그중 하나가 Review Committee 활동입니다.
Review Committee에서는 머지된 코드를 다시 리뷰해 리뷰어와 작성자에게 피드백을 주고, 리뷰하면서 얻은 지식과 인사이트를 Weekly Report라는 이름으로 매주 공유하고 있습니다. 이 Weekly Report 중 일반적으로 널리 적용할 수 있는 주제를 골라 블로그에 코드 품질 개선 기법 시리즈를 연재하고 있습니다.
이번에 블로그로 공유할 Weekly Report의 제목은 '확인 여부를 확인했나요?'입니다.
확인 여부를 확인했나요?
다음 함수는 진행률 표시줄을 표시하는 함수입니다.
/**
* Shows a progress bar blocking other UI interaction with a given progress
* ratio in \[0, 1\].
*/
fun showProgressBar(progress: Float)
진행률 표시줄은 0에서 1 사이의 값만 허용하도록 다음과 같이 구현했습니다. 이 중 caller
는 showProgressBar
를 사용하는 측의 코드입니다. 또한 coerceAtMost
와 coerceAtLeast
는 각 수치를 상한과 하한에 맞추기 위한 함수입니다(max
, min
과 같습니다).
fun caller() {
... // snip
val cappedProgress = progress.coerceAtMost(1F)
showProgressBar(cappedProgress)
}
fun showProgressBar(progress: Float) {
val progressInRange = progress.coerceAtLeast(0F)
... // snip
}
이 코드의 문제점은 무엇일까요?
누군가가 확인할 테니까 괜찮아
위 코드는 값이 [0, 1] 범위인지 확인하는 책임의 소재가 명확하지 않다는 문제가 있습니다. 상한값은 coerceAtMost
를 통해 호출자가 지정하는 반면, 하한값은 coerceAtLeast
를 통해 호출 대상이 지정합니다. 이런 함수는 잘못 사용하기 쉬울 뿐 아니라 사양 및 구현 변경 시 버그가 발생하기도 쉽습니다.
기본적으로 함수(또는 메서드) 호출은 '풀 프루프(fool proof)'여야 합니다. 이와 같이 구현하기 위한 두 가지 방법을 소개하겠습니다.
첫 번째 방법: 믿을 수 있는 것은 자기 자신뿐
첫 번째 방법은 함수의 호출 대상 내에서 확인하는 것입니다. 이 방법은 확인된 혹은 확인되지 않은 상태를 타입 안전(type safe)하게 처리할 수 없는 상황에서 특히 효과적입니다.
예를 들어 일반적인 Float
는 변역이 [0, 1]로 제한되지 않기 때문에 인수로 주어진 Float
가 항상 [0, 1] 범위 내에 있다고 보장하기 어려울 것입니다. 인수는 사용자가 직접 입력한 값일 수도 있고, 외부 시스템에서 제공된 값일 수도 있는데요. 대부분의 경우 인수는 신뢰할 수 없다고 생각해야 합니다.
다음 구현에서는 [0, 1] 범위를 벗어난 값이 입력되면 각각 0, 1로 처리합니다.
fun showProgressBar(progress: Float) {
val progressInRange = progress.coerceIn(0F, 1F)
... // snip
}
호출자에서 범위를 벗어난 값에 대한 오류 처리를 다르게 하고 싶다면 '제대로 처리됐는지 여부'를 반환값으로 알려주는 방법도 있습니다. 다음 코드에서는 범위를 벗어난 인수가 입력된 경우 false
를 반환합니다.
fun showProgressBar(progress: Float): Boolean {
if (progress !in 0F..1F) {
return false
}
... // snip
return true
}
만약 오류가 발생할 수 있다는 것을 호출자에게 알리고 싶다면 호출자에게 반환값 처리를 강제하는 방법(Java Error Prone 라이브러리의 @CheckReturnValue
나 Android Kotlin의 @CheckResult
, Swift나 Rust의 일반 함수)이나 확인된 예외를 사용하는 방법 등이 있습니다.
두 번째 방법: 타입 안전 '검사 필증'
두 번째 방법은 값이 특정 범위에 속한다는 것을 보장하는 타입을 만들어 showProgressBar
에 올바른 값만 전달되도록 만드는 것입니다. showProgressBar
의 경우 다음과 같이 '검사 완료'를 나타내는 모델을 만들면 좋을 것입니다.
class ProgressRatio(rawValue: Float) {
val value = rawValue.coerceIn(0F..1F)
}
fun showProgressBar(progressRatio: ProgressRatio)
범위를 벗어난 값에 대한 오류 처리를 호출자에서 하고 싶다면 다음과 같이 팩토리 함수를 만들거나 실패 가능한 초기화(failable initializer)를 만드는 것도 좋은 방법입니다.
class ProgressRatio private constructor(val value: Float) {
companion object {
fun of(value: Float): ProgressRatio? =
if (value in 0F..1F) ProgressRatio(value) else null
}
}
이때 팩토리 함수에서 오류를 나타내는 값은 타입 안전한 값('안전한' null
이나 Optional
등)을 사용하면 견고한 코드가 됩니다. 반면 확인되지 않은 예외(IllegalArgumentException
등)를 사용할 경우 자세한 내용을 알지 못하면 사용할 수 없는 클래스가 됩니다.
한 줄 요약: 암묵적으로 '확인됐다'는 것을 전제로 하는 코드는 가능한 한 피하는 것이 좋다.
키워드:
implicit dependency
,state check logic
,type safety