Effective Kotlin: 변수의 스코프를 최소화하라

바이너리·2022년 6월 6일
1

Effective Kotlin

목록 보기
1/1
post-thumbnail

Scope란

Scope란 요소를 이용할 수 있는 프로그램 영역을 의미합니다.
코틀린에서는 스코프가 중괄호로 정의되며, 내부에서 외부의 방향으로만 접근이 가능합니다.

val a = 1

fun fizz() {
    val b = 2
    println(a + b)
}

fun buzz() {
    val c = 3
    println(a + c)
}

fizz()
buzz()
println(a)
// println(b)는 불가능합니다.

예시에서 fizz, buzz는 외부 스코프에 있는 변수 a에 접근할 수 있지만, 그 반대는 불가능합니다.


변수 스코프를 좁게 설정해야 할 이유

    var users = listOf("binary", "yun", "ijin")

    var user: String
    for (i in users.indices) {      // bad example
        user = users[i]
        println("User at $i is $user")
    }

    for (i in users.indices) {      // good example
        val user = users[i]
        println("User at $i is $user")
    }

    for ((i, user) in users.withIndex()) {  // best example
        println("User at $i is $user")
    }

첫 번째 예시에서 변수는 for 반복문 스코프의 내부 뿐 아니라 외부에서도 접근 가능합니다.
반면 두 번째와 세 번째 예시에서는 user 스코프를 for 반복문 내부로 제한합니다.

이 때 변수는 최대한 스코프를 좁게 설정하는 것이 좋습니다.

이는 프로그램을 추적하고 관리하기 쉽기 때문입니다.
코드를 분석하면서 어떤 시점에 어떤 요소가 존재하는지 파악할 때, 프로그램에 변경 가능한 부분이 많아지면 이해하기 어려워집니다.

특히 읽고 쓰기 가능한 mutable 프로퍼티는 좁은 스코프에 걸쳐 있을수록 변경을 추적하기 용이해집니다. 또한 변수 스코프 범위가 너무 넓으면 다른 개발자들도 해당 변수에 접근해서 오용할 가능성이 있습니다.

// bad example
val user: User
if (hasValue) { user = getValue() } else { user = User() }

//good example
val user: User = if(hasValue) { getValue() } else { User() }

변수는 읽기/쓰기 가능 여부에 상관없이 변수를 정의할 때 초기화되는 것이 좋고, 여러 프로퍼티를 한꺼번에 설정할 때는 구조분해 선언을 활용하는 것이 좋습니다.

fun updateWeather(degree: Int) {		// bad example
    val description: String
    val color: Color

    if (degree < 5) {
        description = "cold"
        color = Color.BLUE
    } else if (degree < 23) {
        description = "mild"
        color = Color.YELLOW
    } else {
        description = "hot"
        color = Color.RED
    }
}

fun updateWeatherByDestructuringDeclaration(degree: Int) {		//good example
    val (description, color) = when {
        degree < 5 -> "cold" to Color.BLUE
        degree < 23 -> "mild" to Color.YELLOW
        else -> "hot" to Color.RED
    }
}

enum class Color {
    BLUE, YELLOW, RED
}

캡처링

변수의 스코프가 넓을 때 발생할 수 있는 에러의 예시에 대해 알아봅니다.

fun simpleExample() {
    var numbers = (2..30).toList()
    val primes = mutableListOf<Int>()
    while (numbers.isNotEmpty()) {
        val prime = numbers.first()
        primes.add(prime)
        numbers = numbers.filter { it % prime != 0 }
    }
    println(primes)
    // [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
}

fun sequenceExample() {
    val primes: Sequence<Int> = sequence {
        var numbers = generateSequence(2) { it + 1 }

        while (true) {
            val prime = numbers.first()
            yield(prime)
            numbers = numbers.drop(1).filter { it % prime != 0 }
        }
    }
    println(primes.take(10).toList())
    // [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
}

fun sequenceBadExample() {
    val primes: Sequence<Int> = sequence {
        var numbers = generateSequence(2) { it + 1 }

        var prime: Int
        while (true) {
            prime = numbers.first()
            yield(prime)
            numbers = numbers.drop(1).filter { it % prime != 0 }
        }
    }
    println(primes.take(10).toList())
    // [2, 3, 5, 6, 7, 8, 9, 10, 11, 12]
}

첫 번째 예시는 일반적인 간단한 방법(에라토스테네스의 체)로 소수를 구한 예시
두 번째 예시는 시퀀스를 활용해서 리팩토링 한 예시입니다.

세 번째에서는 (잘못된 방식으로) 두 번째 예시를 리팩토링 한 것인데요, 하지만 의도한 대로 출력되지 않았습니다.

그 이유는 무엇일까요?
두 번째 예시에서는 prime 변수가 while 스코프 내에서 반복적으로 할당/해제 되었습니다. 그래서 외부 변수로 빼내어 한 번만 생성하도록 변경한 것입니다.

이처럼 (주로) 람다 본문식에서 외부 변수나 글로벌 변수를 사용하는 행위를 캡처링이라고 합니다.

잘못된 결과가 나온 이유도 prime 변수를 캡처했기 때문인데요, 반복문 내부의 filter 메서드에서는 prime으로 나눌 수 있는 숫자를 필터링 하게 됩니다.
하지만 시퀀스를 사용하기 때문에 최종 연산을 수행하기 전 까지 필터링이 지연됩니다.
그래서 최종적인 prime 값으로만 필터링 되는데, prime이 2일 때 필터링된 4를 제외하면 drop만 동작하여 연속된 숫자를 가져오도록 잘못 동작합니다.

가변성을 피하고 스코프 범위를 좁게 설정하면 잠재적인 캡처 문제를 예방할 수 있습니다.

결론

변수의 스코프는 좁게 설정하는 것이 좋으며, var보다는 val을 사용하는 것이 좋습니다.
람다에서는 외부 변수를 캡쳐해서 사용하는 것에 유의합시다.

profile
01101001011010100110100101101110

0개의 댓글