코틀린 스터디/이펙티브 코틀린

아이템5) 예외를 활용해 코드에 제한을 걸어라

막이86 2024. 2. 14. 18:18
728x90

이펙티브 코루틴을 요약한 내용입니다.

  • 예외를 활용해 제한을 걸어주는 것이 좋음
  • 코틀린에서 코드의 동작에 제한을 걸 때 다음과 같은 방법을 사용할 수 있음
    • require 블록: 아규먼트를 제한할 수 있음
    • check 블록: 상태와 관련된 동작을 제한할 수 있음
    • assert 블록: 어떤 것이 true인지 확인 가능
      • assert 블록은 테스트 모드에서만 동작
    • return 또는 throw와 함께 활용하는 Elvis 연산자
  • 사용 예제
    • 제한을 걸면 문서를 읽지 않은 개발자도 문제를 확인할 수 있음
    • 문제가 있을 경우에 함수가 예상하지 못한 동작을 하지 않고 예외를 발생
    • 코드가 어느 정도 자체적으로 검사를 함
    • 스마트 캐스트 기능을 활용할 수 있음
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
}

아규먼트

  • 아규먼트에 제한을 거는 예
    • 숫자를 아규먼트로 받아서 팩토리얼을 계산한다면 숫자는 양의 정수여야 함
    • 좌표들을 아규먼트로 받아서 클러스터를 찾을 때는 비어 있지 않은 좌표 목록이 필요
    • 사용자로부터 이메일 주소를 입력받을 때는 값이 입력되어 있는지, 이메일 현식이 올바른지 확인
  • 제한을 걸 때는 require 함수를 사용
    • 제한을 만족하지 못할 경우 throw 발생
    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을 발생
  • 람다를 활용해 메시지를 정의할 수 있음
fun factorial(n: Int): Long {
	require(n >= 0) {
		"Cannot calculate factorial of $n because it is samller than 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와 비슷하지만, 지정된 예측을 만족하지 못할 때 IllegalArgumentException 발생

Assert 계열 함수 사용

  • 함수가 올바르게 구현되었다면 확실하게 참을 낼 수 있는 코드
  • 구현 문제로 발생할 수 있는 문제를 예방하려면, 단위 테스트를 사용하는 것이 좋음
class StackTest {
	@Test
	fun `Stack pops correct number of elements`() {
		val stack = Stack(20) { it }
		val ret = stack.pop(10)
		assertEquals(10, ret.size)
	}
}
  • 단위 테스트는 구현의 정확성을 확인하는 가장 기본적인 방법
  • pop 함수 내부에서 Assert 계열의 함수를 사용
    • 코틀린/JVM에서만 활성화 됨
      • -ea JVM 옵션 활성화
    fun pop(num: Int = 1): List<T> {
    	assert(ret.size == num)
    	return ret
    }
    
  • 프로덕션 환경에서는 오류가 발생하지 않음
  • 코드가 정말 심각한 오휴고, 심각한 결과를 초래할 수 있는 경우에는 check를 사용하는 것이 좋음
  • assert 장점
    • Assert 계열의 함수는 코드를 자체 점검하며, 더 효율적으로 테스트할 수 있음
    • 특정 상황이 아닌 모든 상황에 대한 테스트를 할 수 있음
    • 실행 시점에 정확하게 어떻게 되는지 확인
    • 실제 코드가 더 빠른 시점에 실패하게 만듬
      • 예상하지 못한 동작이 언제 어디서 실행되었는지 쉽게 찾을 수 있음
  • assert를 활용해도 여전히 단위 테스트는 따로 작성

nullability와 스마트 캐스팅

  • 코틀린에서 require와 check 블록으로 어떤 조건을 확인해서 true가 나왔다면, 해당 조건은 이후로도 true일거라고 가정
public inline fun require(value: Boolean): Unit {
	contract {
		returns() implies value
	}
	require(value) {
		"Failed requirement."
	}
}
  • require와 check를 활용해서 타입 비교를 했다면 스마트 캐스트가 동작
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 = preson.email
}
  • requireNotNull, checkNotNull이라는 특수한 함수를 사용해도 괜찮음
class Person(val email: String?)
fun validateEmail(email: String) { }

fun sendEmail(person: Person, message: String) {
	val email = requireNotNull(person.email)
	validateEmail(email)
}

fun sendEmail(person: Person, text: String) {
	requireNotNull(person.email)
	validateEmail(person.email)
}
  • nullability를 목적으로 오른쪽에 throw 또는 return을 두고 Elvis 연산자를 확용하는 경우가 많음
    • 오류를 발생하지 않고 단순하게 함수를 중지할 수도 있음
    • 함수가 중지된 이유를 로그에 출력해야 할 때 사용할 수 있음
fun sendEmail(person: Person, text: String) {
	val email: String = person.email ?: run {
		log("Email not sent, no email address")
		return 
	}
}

정리

  • 제한을 훨씬 더 쉽게 확인할 수 있다
  • 애플리케이션을 더 안정적으로 지킬 수 있다
  • 코드를 잘못 쓰는 상황을 막을 수 있다
  • 스마트 캐스팅을 활용할 수 있다
728x90