확실하게 어떤 형태로 동작해야 하는 코드가 있다면, 예외를 활용해 제한을 걸어주는 것이 좋다.
코틀린에서는 코드의 동작에 제한을 걸때 다음과 같은 방법을 사용할 수 있다.
- require
- check
- assert(테스트 에서만 작동)
- return 또는 throw와 함께 사용하는 Evlis operator
fun pop(num: Int = 1): List<T> {
require(num <= size) {
"Cannot remove more elements than current size"
}
// 생략
}
위와 같이 제한을 걸어주면 아래와 같은 장점이 존재한다.
- 제한을 걸면 문서를 읽지 않은 개발자도 문제를 확인할 수 있다.
- 문제가 있을 경우 함수가 예상하지 못한 동작을 하지 않고 예외를 throw 한다. throw를 하지 않고 예상하지 못한 동작을 하는 것은 예외를 throw 하는 것 보다 위험하며, 상태를 관리하기 어렵다.
- 코드가 어느정도 자체적으로 검사되어 위와 관련된 테스트를 줄일 수 있다.
- 스마트 캐스트를 활용할 수 있게 되어, 캐스트를 적게 할 수 있다.
아규먼트
함수를 정의할 때 타입 시스템을 활용해 아래와 같이 argument에 제한을 거는 코드를 많이 사용한다.
- 숫자를 아규먼트로 받아 팩토리얼을 계산한다면 숫자는 양의 정수여야 한다.
- 좌표들을 아규먼트로 받아서 클러스터를 찾을 때는 비어있지 않은 좌표 목록이 필요하다.
- 사용자로부터 이메일 주소를 받을 때 값이 입력되어 있는지, 형식이 올바른지 체크해야 한다.
일반적으로 이러한 제한을 걸 때 require 함수를 사용한다.
fun factorial(n: Int): Long {
require(n >= 0)
//
}
상태
아래와 같은 경우, 어떤 구체적인 조건을 만족할 때만 함수를 사용할 수 있게 해야 한다.
- 어떤 객체가 미리 초기화되어 있어야만 처리를 하게 하고 싶은 함수
- 사용자가 로그인했을 때만 처리를 하게 하고 싶은 함수
- 객체를 사용할 수 있는 시점에 사용하고 싶은 함수
위와 같이 상태와 관련된제한을 할 때는 일반적으로 check를 사용한다.
fun speak(text: String) {
check(isInitialized)
// ..
}
이러한 확인은 사용자가 규약을 어기고 사용하면 안되는 곳에서 함수를 호출하고 있다고 의심될 때 한다.
사용자가 코드를 제대로 사용할거라고 믿기 보다는 문제 상황을 예측하고, 문제 상황에 예외를 throw 하는 것이 좋다.
Assert 계열 함수 사용
단위 테스트에서의 Assert 계열 함수 사용에 대해 다룬다.
자세한 내용은 생략
nullability와 스마트 캐스팅
코틀린에서 require와 check로 조건을 확인해 true가 나오면 해당 조건은 이후로도 true일 거라고 가정한다.
따라서 이를 활용해 타입 비교를 한다면 스마트 캐스트가 작동한다.
- Smart cast
fun changeDress(person: Person) {
require(person.outfit is Dress)
val dress: Dress = person.outfit // person.outfit은 스마트 캐스트로 인해 Dress 타입 보장
// ..
}
- requireNotNull, checkNotNull을 활용한 unpack
위와 같은 특징은 대상이 null인지 체크하는데 유용하며, 이 경우 requireNotNull, checkNotNull이라는 특수한 함수를 사용해도 좋다.
둘 다 스마트 캐스트를 지원하므로 변수를 언팩(unpack)하는 용도로 활용할 수 있다.
class Person(val email: String?)
fun validateEmail(email: String) { }
fun sendEmail(person: Person, text: String) {
val email = requireNotNull(person.email)
validateEmail(email)
}
- Elvis operator 활용
nullability를 목적으로 오른쪽에 throw 혹은 return을 두고 Elvis operator를 활용하는 경우가 많다.
이러한 코드는 굉장히 읽기 쉽고, 유연하게 사용할 수 있다.
첫번째로 오른쪽에 return을 넣으면 오류 없이 단순히 함수를 중지할 수도 있음
fun sendEmail(person: Person, text: String) {
val email: String = person.email ?: return
}
- return / throw와 run 활용
프로퍼티에 문제가 있어서 null 일 때 여러 처리가 필요한 경우 return / throw와 run을 조합해 사용할 수 있다.
이는 함수가 중지된 이유를 로그에 출력해야 할 때 사용할 수 있다.
fun sendEmail(person: Person, text: String) {
val email: String = person.email ?: run {
log("Email not sent, no email adress")
return
}
}
정리
이번 절에서 활용한 내용을 기반으로 아래와 같은 이득을 얻을 수 있다.
- 제한을 훨씬 더 쉽게 확인할 수 있다.
- 애플리케이션을 더 안정적으로 지킬 수 있다.
- 코드를 잘못 쓰는 상황을 막을 수 있다.
- 스마트 캐스팅을 활용할 수 있다.
활용한 메커니즘
- require block
- check block
- assert block
- return, throw, elvis operator
Reference
- 이펙티브 코틀린 - 프로그래밍 인사이트, 마르친 모스칼라 지음, 윤인성 옮김
개인적인 기록을 위해 작성된 글이라 잘못된 내용이 있을 수 있습니다.
오류가 있다면 댓글을 남겨주세요.