[이펙티브 코틀린] 1장 안정성 Item 5 예외를 활용해 코드에 제한을 걸어라

Sdoubleu·2023년 1월 5일
0

이펙코

목록 보기
5/7
post-thumbnail

코드의 동작에 제한을 걸 때 사용하는 방법

  1. require 블록: 아규먼트를 제한할 수 있음
  2. check 블록: 상태와 관련된 동작을 제한할 수 있음
  3. assert 블록: 어떤 것이 true 인지 확인 할 수 잇음
    assert 블록은 테스트 모드에서만 작동
  4. return 또는 throw와 함께 활용하는 Elvis 연산자

🛠️ require에 대한 정보
Elvis 연산자에 대한 정의와 예시

ex)

// Stack<T>의 일부
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
}

제한의 다양한 장점

  1. 제한을 걸면 문서를 읽지 않는 개발자도 문제를 확인 가능
  2. 문제가 있을 경우 함수가 예상하지 못한 동작을 하지 않고 예외를 throw 함
    예상하지 못한 동작은 하는 것은 예외를 throw하는 것보다 굉장히 위험
    -> 상태 관리가 힘들어짐
  3. 코드가 어느 정도 자체적으로 검사됨
    -> 단위 테스트를 줄일 수 있음 !
  4. 스마트 캐스트 기능을 활용할 수 있게 됨
    -> 캐스트(타입 변환)를 적게 할 수 있음

아규먼트(argument)

parameter & argument의 정의

함수를 정의할 때 타입 시스템을 활용해서 아규먼트(argument)에 제한을 거는 코드를 많이 사용

ex)
1. 숫자를 아규먼트로 받아서 팩토리얼을 계삲나다면 숫자는 양의 정수여야 합니다.
2. 좌표들을 아규먼트로 받아서 클러스터를 찾을 때는 비어 있지 않은 좌표 목록이 필요합니다.
3. 사용자로부터 이메일 주소를 입력받을 때는 값이 입력되어 있는지, 그리고 이메일 형식이 올바른지 확인해야 합니다.

일반적으로 제한을 걸 대는 require 함수를 사용
require 함수는 제한을 확인하고, 제한을 만족하지 못할 경우 예외를 throw

require() & check() 함수의 정의

fun factorial(n: Int): Long {
	require(n >= 0)
    return if (n <= 1) 1 else factorial(n - 1) * n
}
//...
fun findClusters(points: List<Point>): List<Clusters> {
	require(points.isNotEmpty())
    //...
}
//...
fun sendEmail(user: User, message: String) {
	requireNotNull(user.email)
    require(isValidEmail(user.email))
}

↪ 입력 유효성 검사 코드는 함수의 가장 앞부분에 배치되므로,
읽는 사람도 쉽게 확인할 수 있음


- **require** 함수는 조건을 만족하지 못할 때 무조건적으로 **IllegalArgument Exception**을 발생시키므로 제한을 무시 못함
  • 람다를 활용해서 지연 메시지를 정의 가능
fun factorial(n: Int): Long {
	require(n >= 0) { "Cannot calculate factorial of $n " +
"because it is smaller than 0" }
	return if(n <= 1) 1 else factorial(n - 1) * n
}

상태

어떤 구체적인 조건을 만들 때만 함수를 사용할 수 있게 해야할 때가 있음

  1. 어떤 객체가 미리 초기화되어 있어야만 처리를 하게 하고 싶은 함수
  2. 사용자가 로그인했을 때만 처리를 하게 하고 싶은 함수
  3. 객체를 사용할 수 있는 시점에 사용하고 싶은 함수

상태와 관련된 제한을 걸 때는 일밙거으로 check 함수를 사용

fun speak(text: String) {
	check(isInitialized)
  	//...
}
//...
fun getUserInfo(): UserInfo {
	checkNotNull(token)
  	//...
}
//...
fun next(): T {
  	check(isOpen)
  	//...
}
  • check 함수require와 비슷하지만, 지정된 예측을 만족하지 못할 때
    -> IllegalStateExceptionthrow
    -> 상태가 올바른지 확인할 때 사용
  • 예외 메시지는 require와 마찬가지로 지연 메시지를 사용해서 변경 가능
  • 함수 전체에 대한 어떤 예측이 있을 때는 일반적으로 require 블록 뒤에 배치
    -> check를 나중에 하는 것
  • 스스로 구현한 내용을 확인할 때는 일반적으로 assert라는 함수 사용

Assert 계열 함수 사용

구현 문제로 발생할 수 있는 추가적인 문제를 예방하려면
-> 단위 테스트를 사용하는 것이 좋음👍

class StackTest {
	@Test
  	fun 'Stack pops correct number of elements'() {
  	val stack = Stack(20) {it}
  	val ret = stack.pop(10)
  	asserEquals(10, ret.size)
  	}
  	//...
}
  • 단위 테스트는 구현의 정확성을 확인하는 가장 기본적인 방법

ex) 현재 코드에서 스택이 10개의 요소를 pop하면, 10개의 요소가 나온다는 보편적인 사실을 테스트하고 있다.
모든 pop 호출 위치에서 제대로 동작하는지 확인하기 위해
pop 함수 내부에서 Assert 계열의 함수를 사용

fun pop(num: Int = 1): List<T> {
  	//...
  	assert(ret.size == num)
  	return ret
}

↪ 이러한 조건은 코틀린/JVM에서만 활성화

Assert 장점

  1. Assert 계열의 함수는 코드를 자체 점검
    -> 더 효율적으로 테스트
  2. 특정 상황이 아닌 모든 상황에 대한 테스트 가능
  3. 실행 시점에 정확하게 어떻게 되는지 확인 가능
  4. 실제 코드가 더 빠른 시점에 실패하게 만듦
    -> 예상하지 못한 동작이 언제 어디서 실행되었는지 쉽게 찾을 수 있음👍

➕이를 활용해도 단위 테스트는 따로 작성해야 함
표준 애플리케이션 실행에서는 assert가 예외를 throw하지 않는다는 것


nullability와 스마트 캐스팅

코틀린에서 requirecheck 블록으로 어떤 조건을 확인해서 true가 나왔다면,
해당 조건은 이후로도 true일 거라고 가정

public inline fun require(value: Boolean): Unit {
	contract {
		returns() implies value
  	}
  	require(value) {" Failed requirement."}
}

↪ 이를 활용해서 타입 비교를 했다면, 스마트 캐스트가 작동

ex) 어떤 사람(person)의 복장(person.outfit)이 드레스(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이라는 특수한 함수를 사용해도 괜찮음
둘 다 스마트 캐스트를 지원하므로, 변수를 '언팩(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)
  	//...
}

nullability를 목적으로, 오른쪽에 throw 또는 return을 두고 Elvis 연산자를 활용하는 경우가 많음

첫 번째로 오른쪽에 return을 넣으면, 오류를 발생시키지 않고 단순하게 함수를 중지할 수도 있음

fun sendEmail(person: Person, text: String) {
  	val email: String = person.email ?: return
  	//...
}

프로퍼티에 문제가 있어서 null일 때 여러 처리를 해야 할 때도,
reutrn/throw와 run 함수를 조합해서 활용하면 됨
이는 함수가 중지된 이유를 로그에 출력해야 할 때 사용 할 수 있음

fun sendEmail(person: Person, text: String) {
  	val email: String = person.email ?: run {
  		log("Email not sent, no email address")
  		return
  	}
  	//...
}

return과 throw를 활용한 Elvis 연산자는 nullable을 확인할 때 굉장이 많이 사용되는 관용적인 방법
이러한 코드는 함수의 앞부분에 넣어서 잘 보이게 만드는 것이 좋음


⭐정리

  1. 제한을 훨씬 더 쉽게 확인할 수 있음
  2. 애플리케이션을 더 안정적으로 지킬 수 있음
  3. 코드를 잘못 쓰는 상황을 막을 수 있음
  4. 스마트 캐스팅을 활용할 수 있음
  • require 블록: 아규먼트와 관련된 예측을 정의할 때 사용하는 범용적인 방법
  • check 블록: 상태와 관련된 예측을 정의할 때 사용하는 범용적인 방법
  • assert 블록: 테스트 모드에서 테스트를할 때 사용하는 범용적인 방법
  • return과 throw와 함께 Elivs 연산자 사용하기

🔗참고자료

https://wayhome25.github.io/etc/2017/12/31/parameter-argument/

https://seosh817.tistory.com/155

profile
개발자희망자

0개의 댓글