확시하게 어떤 형태로 동작해야 하는 코드가 있다면 예외를 활용해 제한을 걸어주는 것이 좋습니다
동작 제한 방법
- require 블록 : 아규먼트를 제한할 수 있습니다.
- check 블록 : 상태와 관련된 동작을 제한할 수 있습니다.
- assert 블록 : 어떤 것이 true인지 확인할 수 있습니다. assert 블록 테스트 모에서만 작동
- return 또는 throw와 함께 활용하는 Elvis 연산자
fun main(num: Int = 1):List<T>{
require(num <= size) {
"Cannot"
}
check(isOpen){"cannot"}
val ret = collection.take(num)
collection = collection.drop(num)
assert(ret.size == num)
return ret
}
이렇게 제한을 걸어주면 다양한 장점이 있습니다.
- 제한을 걸면 문서를 읽지 않은 개발자도 문제를 확인할수 있습니다.
- 문제가 있을 경우에 함수가 예상하지 못한 동작을 하지 않고 예외를 throw합니다. 예상하지 못한 동작을 하는 것은 예외를 throw하는 것보다 굉장히 위험하며 상태를 관리하는 것이 굉장히 힘듭니다. 이러한 제한으로 인해서 문제를 놓치지 않을 수 있고 코드가 안정적으로 작동합니다.
- 코드가 어느 정도 자체적으로 검사됩니다. 따라서 이와 관련된 단위 테스트를 줄일 수 있습니다.
- 스마트 캐스트 기능을 활용할 수 있게 되므로 캐스트를 적게할 수 있습니다.
야규먼트에 제한을 거는 예
- 숫자를 아규먼트로 박아서 팩토리얼을 계산한다면 숫자는 양의 정수여야 합니다
- 좌표들을 야규먼트로 받아서 클리스터를 찾을 때는 비어 있지 않은 좌표 목록이 필요 합니다.
- 사용자로부터 이메일 주소를 입력받을 때는 값이 입력되어 있는지, 그리고 이메일 형식이 올바른지 확인해야 합니다.
일반적으로 이러한 제한을 걸 때는 require함수를 사욯합니다. require함수는 제한을 확인하고, 제한을 만족하지 못할 경우 예외를 throw합니다.
fun factorial(num: Int = 1):Long{
require(n >= 0)
return if (n <= 1) 1 else factorial(n - 1) * n
}
fun findClusters(point:List<Point>) : List<Cluster>{
require(point.isNotEmpty())
// ..
}
fun senEmail(user: User, message:String){
requireNotNull(user.email)
return(isValidEmail(user.email))
// ..
}
이와 같은 형태의 입력 유효성 검사 코드는 함수의 가장 앞부분에 배치되므로 읽는 사람도 쉽게 확인할 수 있습니다.(물론 문서에 관한 내용은 따로 명시해야 합니다)
require 함수는 조건을 만족하지 못할 때 무조건적으로 IllegalArgument Exception을 발생시키므로 제한을 무시할 수 없습니다. 일반적으로 이러한 처리는 앞부분에 하게 되므로, 코드를 읽을 때 쉽게 확인할 수 없습니다. -- 문서의 별도 표시 필수
람다를 활용해서 지연 메시지를 정의할 수도 있습니다.
fun factorial(n:Int):Long{
require(n >= 0){"not $n" + "be 0"}
return if (n <= 1) 1 else factorial(n - 1) * n
}
이외에도 예외를 활용해 제한을 거는 상태가 있습니다.
상태 구체적인 조건을 만족할 때만 함수를 사용할 수 있게 해야 할 때가 있습니다 예
- 어떤 객체가 미리 초기화되어 있어야만 처리를 하게 하고 싶은 함수
- 사용자가 로그인 했을 때만 처리를 하게 하고 싶은 함수
- 객체를 사용할 수 있는 시점에 사용하고 싶은 함수
상태와 관련된 제한을 걸 때는 일반적으로 check 함수를 사용합니다.
fun speak(text: String) {
check(isInitialized)
//..
}
fun getUserInfo(): UserInfo{
checkNotNull(token)
// ..
}
fun next():T {
check(isopen)
// ..
}
check는 require와 비슷하지만 지정된 예측을 만족하지 못할 때 IllegalArgument Exception을 throw 합니다. 상태가 올바른지 학인할 때 사용합니다. 예외 메세지는 require와 미찬가지로 지연 메세지를 사용하여 변경할수 있습니다. 함수 전체에 대한 어떤 예측이 있을 때는 일반적으로 require 블록 뒤에 배치 합니다. - check를 나중에
스스로 수현한 내용을 확인할 때는 일반적으로 assert라는 함수를 사용합니다.
Assert
함수가 올바르게 구현 되어 있지 않을 수도 있습니다 이러한 구현문제로 발생할 수 있는 추가적은 문제를 예방하려며, 단위 테스트를 사용하는 것이 좋습니다.
class StackTest {
fun 'stack'() {
val stack = Stack(20) {it}
val ret = stack.pop(10)
assertEquals(10, ret.size)
}
// ...
}
현재 코드에서 스택이 10개의 요소를 팝(pop)하면, 10개의 요소가 나온다는 보편적인 사실을 테스트하고 있습니다. 하지만 현재와 같이 한 경우만 테스트해서 모든 상황에서 괜찮은지 알 수 없습니다. 따라서 모든 pop 호출 위치에서 재대로 동작하는지 확인하는것이 좋습니다 pop함수 내부에 Assert 계열의 함수를 사용해 봅시
fun pop(num: Int = 1):List<T> {
// ..
assert(ret.size == num)
return ret
}
이러한 조건은 코틀린/JVM에서만 활성화되며, -ea JVM옵션응 활성화해야 확인할 수 있습니다. 그래서 프로덕션 환경에서는 오류가 발생하지 않습니다 테스트 할 때만 활성화되므로 오류가 발생해도 사용자가 알아차릴수 없습니다. 그래서 check를 사용하는 것이 좋습니다.
단위테스트에서 assert를 사용하면 좋은점
- Assert 계열의 함수는 코드를 자체 점검하며, 더 효율적으로 테스트할 수 있습니다.
- 특정 상황이 아닌 모든 상황에 대한 테스트를 할 수 있습니다.
- 실행시점에 정확하게 어떻게 되는지 확인할 수 있습니다.
- 실제코드가 더 빠른 시점에 실패하게 만듭니다. 따라서 예상하지 못한 동작이 언제 어디서 실행되었는지 쉽게 찾을 수 있습니다.
참고로 이를 활용해서 단위테스트는 따로 작성해야 합니다. 표준 애플리케이션 실행에서는 assert가 예외를 throw하지 않습니다 파이썬에서 사용 많고 자바에서 사용 x 코틀린 코드를 안정적으로 만들고 싶을 때 양념 처럼 사용
nullability와 스마트 캐스팅
코틀린에서 require와 check 블록으로 어떤 조건을 확인해서 true가 나오면 해당 조건은 이후로도 true일 거라고 가정합니다
public inline fun require(value: Boolean): Unit{
contract {
returns() implies value
}
require(value) {"Failed"}
}
이를 활용해 타입 비교를 했다면 스마트 캐스트가 작동 합니다 다음 예는 person.outfilt이 dress여야 코드가 정상적으로 진행됩니다. 이러한 outfit프로퍼티가 final이라면 outfit 프로퍼티가 dress로 스마트 캐스트 됩니다.
fun changeDress(person:Person){
require(person.outfit is Dress)
val dress: Dress = person.outfit
// ..
}
이러한 특징은 null인지 확인할 때 유용 합니다.
class Person(val email: String?)
fun sendEmail(person: Person, message:String){
require(person.email != null)
val email: String = person.email
//...
}
이러한 경우 requireNotNull, checkNotNull이라는 특수한 함수를 사용해도 괜찮습니다. 둘 다 스마트 캐스트를 지원하므로 변수를 언팩하는 용도로 활용할 수 있습니다.
class Person(val email: String?)
fun validateEmail(email: String) {}
fun sendEmail(person: Person, text:String){
val email = requireNotNull(person.email)
validateEmail(email)
//...
}
fun sendEmail(person: Person, text:String){
requireNotNull(person.email)
validateEmail(person.email
//..)
}
nullability를 목적으로, 오른쪽에 throw또는 return을 두고 Elvis 연산자를 활용하는 경우가 많습니다. 이런한 코드는 굉장히 읽기 쉽고, 유연하게 사용할수 있습니다. 첫번째로 오른쪽에 return을 넣으면 오류를 발생시키지 않고 단순하게 함수를 중지할 수도 있습니다.
fun sendEmail(person: Person, text:String){
val email:String = person.email ? : return
validateEmail(email)
//...
}
프로퍼티에 문제가 있어서 null일때 여러 처리를 해야 할 때도 return/throw와 run 함수를 조합해서 활용하면 됩니다. 이는 함수가 중지된 이유를 로그에 출력해야 할 때 사용할 수 있습니다.
fun sendEmail(person: Person, text:String){
val email:String = person.email ? : run{
log("Email")
return
}
//...
}
이러한 코듣는 함수의 앞부분에 넣어 잘 보이게 만드는 것이 좋습니다.
정리
이점
- 제한을 훨씬 더 쉽게 확인할 수 있다
- 애플리케이션을 더 안정적으로 지킬 수 있다
- 코드를 잘못 쓰는 상황을 막을 수 있다
- 스카트 캐스팅을 활용할 수 있다.
매커니즘
- require 블록 : 아규먼트와 관련된 예측을 정의할 때 사용하는 범용적인 방법
- check 블록 : 상태와 관련된 예측을 정의할 때 사용하는 범용적인 방법
- assert 블록 : 테스트 모드에서 테스트를 할 때 사용하는 범용적인 방법
- return과 throw와 함께 Elvis 연산자 사용하기
전달 인자(argument)는 함수를 호출할 때 전달되는 실제 값
엘비스 연산자는 ?:로 표현하며, ?:의 왼쪽 객체가 non-null이면 그 객체의 값이 리턴되고, null이라면 ?:의 오른쪽 값을 리턴합니다. 예 :
fun main(args: Array<String>){
val str: String? = "1234"
val nullStr: String? = null
var len: Int = str?.length ?: -1
println("str.length: $len")
len = nullStr?.length ?: -1
println("nullStr.length: $len")
}
스마트 캐스트
코틀린의 경우 'is'를 사용하여 변수의 타입을 검사한다. 자바의 'instanceof'와 비슷하게 볼 수 있지만, 'instanceof'는 타입을 검사 후 변수 타입을 캐스팅 해주어야 한다. 하지만 'is'는 타입 검사 후 컴파일러가 자동으로 형변환 (캐스팅)을 해준다.
이 기능을 스마트 캐스트 라고 한다.