코틀린에서는 코드의 동작에 제한을 걸 수 있는데, 다음과 같이 쓸 수 있다
require 블록: 아큐먼트를 제한할 수 있다.
check 블록: 상태와 관련된 동작을 제한할 수 있다.
assert 블록: 어떤 것이 true인지 확인할 수 있다. assert 블록은 테스트 모드에서만 작동한다.
return 또는 throw와 함께 엘비스 연산자를 사용한다.
예를 들면, 아래 코드에서처럼 쓰일 수 있는데
fun pop(num: Int = 1): List<T> {
require(num <= size) {
"Cannot remove more elements than current size"
}
check(isOpen) { "Cannot pop from closed stack" }
val ret = collection.take(num)
collection = collection.drop(num)
assert(ret.size == num)
return ret
}
이럴 경우 제한으로 하여금 다양한 장점이 발생한다.
이렇게 제한을 두는게 장점이 많다는 걸 알게 됐는데, 그럼 어떤 제한들이 있는지 더 자세하게 알아보자!
함수를 정의할 때 타입 시스템을 활용해서 아규먼트에 제한을 거는 코드를 많이 사용한다.
ex 1) 숫자를 아규먼트로 받아서 팩토리얼 계산한다면 숫자는 양의 정수여야 한다.
ex 2) 좌표들을 아규먼트로 받아서 클러스터를 찾을 땐 비어 있지 않은 좌표 목록이 필요하다.
ex 3) 사용자로부터 이메일 주소를 입력받을 때는 값이 입력되어 있는지, 그리고 이메일 형식이 올바른지 확인해야 한다.
일반적으로 이러한 제한을 걸 때 require
함수를 사용한다. require 함수는 제한을 확인하고 만족하지 못하면 예외를 던진다.
fun factorial(n: Int): Long {
require(n >= 0)
return if (n <= 1) 1 else factorial(n - 1) * n
}
fun findClusters(points: List<Point>): List<Cluster> {
require(points.isNotEmpty())
// ...
}
fun sendEmail(user: User, message: String) {
requireNotNull(user.email)
require(isValidEmail(user.email))
// ...
}
특히 입력 유효성 검사 코드는 함수 앞부분에 배치되서 읽는 사람도 쉽게 확인할 수 있다.
대신 require 함수는 조건을 만족하지 못할 때 무조건적으로 IllegalArgumentException을 발생시키기 때문에 제한을 무시할 수 없다.
지금까지 본 거처럼 아규먼트와 관련된 제한을 걸 때 사용할 수 있는데 또 대표적으로 상태를 대상으로 제한을 걸 수도 있다.
어떤 구체적인 조건을 만족할 때만 함수를 사용할 수 있게 해야 할 때가 있다.
어떤 객체가 미리 초기화되어 있어야만 처리를 하게 하고 싶은 함수
사용자가 로그인했을 때만 처리를 하게 하고 싶은 함수
객체를 사용할 수 있는 시점에 사용하고 싶은 함수
이 때, 상태과 관련된 제한은
check
함수를 사용하면 된다.
fun speak(text: String) {
check(isInitialized)
// ...
}
fun getUserInfo(): UserInfo {
checkNotNull(token)
// ...
}
fun next(): T {
check(isOpen)
// ...
}
얼핏보면 require
함수와 비슷할 수 있지만, check
함수는 지정된 예측을 만족하지 못할 때, IllegalStateException
을 던진다.
예외 메시지는 require와 마찬가지로 지연 메시지를 사용해서 변경할 수 있다. 그래서 함수 전체에 대한 어떤 예측이 있을 때는 일반적으로 require 블록 뒤에 배치해서 check를 나중에 한다.
그래서 이를 통해 사용자가 규약을 어기고 사용하면 안되는 곳에서 함수를 호출하고 있다고 의심될 때 사용해보자. 이는 사용자뿐만 아니라 구현하는 사람에게도 도움이 된다.
그럼 스스로 구현한 내용을 확인할 때는 어떤 함수를 쓸까?
우리는 함수를 제대로 구현할 수도 있지만 올바르게 구현되지 않을 수도 있다. 이는 처음부터 잘못된 구현일 수도 있고 누군가가 리팩토링하면서 작동하지 않게 된 것일수도 있다.
그래서 이를 방지하기 위해 단위테스트를 아래와 같이 사용을 하는데, 이때 assert
함수를 사용한다.
class StackTest {
@Test
fun `Stack pops correct number of elements`() {
val stack = Stack(20) { it }
val let = stack.pop(10)
assertEquals(10, ret.size)
}
}
또한 현재 테스트 코드 말고도 pop
자체의 함수가 제대로 동작하는지 확인해보고 싶다면 다음처럼 pop
내부 함수에서도 쓸 수 있다.
fun pop(num: Int = 1): List<T> {
// ...
assert(ret.size == num)
return ret
}
이런 코드도 예상대로 동작하는지 확인하므로 테스트라고 할 수 있다. 다만, 프로덕션 환경에서는 오류가 발생하지 않을 수도 있다. 테스트를 할 때만 활성화되므로 오류가 발생해도 눈치채기 어려울 수 있기 때문이다.
만약 이 코드가 심각한 오류고 심각한 결과를 초래할 수 있는 경우에는 check
를 사용하자.
대신 단위 테스트 대신 함수에서 assert를 사용하면 다음과 같은 장점이 있다.
그래도 여전히 단위 테스트는 따로 작성해야 한다. 표준 app 실행에서는 assert
가 예외를 던지지 않는 것도 기억하자.
코틀린에서는 require
와 check
블록으로 어떤 조건을 확인해서 true가 나왔다면, 해당 조건은 이후로도 true일거라고 가정한다.
public inline fun require(value: Boolean): Unit {
contract {
returns() implies value
}
require(value) {
"Failed requirement"
}
}
그래서 요걸 활용해서 타입 비교했다면, 스마트 캐스트가 작동한다.
fun changeDress(person: Person) {
require(person.outfit is Dress)
val dress: Dress = person.outfit
// ...
}
이 코드처럼 사람의 복장이 드레스여야 정상적으로 진행된다. 이때 outfit
프로퍼티가 final
이라면, outfit
프로퍼티가 Dress로 스마트 캐스트된다.
이 특징은 어떤 대상이 null인지 확인할 때 굉장히 유용하다.
class Person(val email: String?)
fun sendEmail(person: Person, message: String) {
require(person.email != null)
val email: String = person.email
}
그리고 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)
}
fun sendEmail(person: Person, text: String) {
requireNotNull(person.email)
validateEmail(person.email)
// ...
}
그리고 다음과 같이 엘비스 연산자를 사용하면 가독성이 좋아지며 유연하게 사용할 수 있다.
fun sendEmail(person: Person, text: String) {
val email: String = person.email ?: return
}
혹은 return/throw run 함수를 조합해서 활용도 가능하다.
fun sendEmail(person: Person, text: String) {
val email: String = person.email ?: run {
log("Email not sent, no email address")
return
}
// ...
}
사실 코틀린 개발하면 썼던 거만 계속 썼는데 이 파트를 통해서 제한하는 함수가 더 많다는 것을 알게 되었다. 이를 통해 적극적으로 개발하며 유지보수에 힘 쓸 수 있도록 한 번 응용해봐도 좋을 듯 하다!