반응형
Notice
Recent Posts
Recent Comments
Link
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
Tags
more
Archives
Today
Total
관리 메뉴

안드로이드 개발자 노트

[이펙티브 코틀린] Item2. 변수의 스코프를 최소화하라 본문

Kotlin/이펙티브 코틀린

[이펙티브 코틀린] Item2. 변수의 스코프를 최소화하라

어리둥절범고래 2023. 8. 19. 14:04
반응형

스코프라는 것은 요소를 볼 수 있는 컴퓨터 프로그램 영역으로, 가시성(visible)을 의미합니다.

상태를 정의할 때는 변수와 프로퍼티의 스코프를 최소화하는 것이 좋습니다.

 

 

  • 프로퍼티보다는 지역 변수를 사용하는 것이 좋다.
  • 최대한 좁은 스코프를 갖게 변수를 사용한다. 예를 들어 반복문 내부에서만 변수가 사용된다면, 변수를 반복문 내부에 작성하는 것이 좋다.
// 나쁜 예
var user: User
for (i in users.indices) {
    user = users[i]
    print("User at $i is $user")
}

// 조금 더 좋은 예
for (i in users.indices) {
    val user = users[i]
    print("User at $i is $user")
}

// 제일 좋은 예
for ((i, user) in users.withIndex()) {
    print("User at $i is $user")
}

 

첫 번째 예에서 변수 user는 for 반복문 스코프 내부뿐만 아니라 외부에서도 사용할 수 있습니다.

반면 두 번째와 세 번째 예에서는 user의 스코프를 for 반복문 내부로 제한하고 있습니다.

 

스코프를 좁게 만드는 이유는 아래와 같습니다.

 

 

  • 프로그램을 추적하고 관리하기 쉽다.
  • 요소가 많아져서 프로그램에 변경될 수 있는 부분이 많아지면 프로그램을 이해하기 어렵다.
  • mutable 프로퍼티는 좁은 스코프에 걸쳐 있을수록, 그 변경을 추적하는 것이 쉽다.
  • 스코프 범위가 너무 넓으면, 다른 개발자에 의해 변수가 잘못 사용될 수도 있다.
  • 변수는 var/val 상관 없이, 변수를 정의할 때 초기화되는 것이 좋다.
  • if, when, try-catch, Elvis 표현식 등을 활용하면, 변수를 정의할 때 초기화할 수 있다.
//나쁜 예
val user: User
if (hasValue) {
    user = getValue()
} else {
    user = User()
}

// 조금 더 좋은 예
val user: User = if(hasValue) {
    getValue()
} else {
    User()
}

// 나쁜 예
fun updateWeather(degrees: Int) {
    val description: String
    val color: Int
    if (degrees < 5) {
        description = "cold"
        color = Color.BULE
    } else if (degrees < 23) { 
        desciption = "mild"
        color = Color.YELLOW
    } else {
        desciption = "hot"
        color = Color.RED
    }
}

// 조금 좋은 예
fun updateWeather(degrees: Int) {
    // 여러 프로퍼티를 한꺼번에 설정해야 하는 경우 구조분해 선언을 활용하는 것이 좋다.
    val (description, color) = when {
        degrees < 5 -> "cold" to Color.BLUE
        degrees < 23 -> "mild" to Color.YELLOW
        else -> "hot" to Color.RED
    }
}

 

 

 

 


캡처링

 

캡처링은 람다식 외부에서 정의한 변수를 참조하는 변수를 람다식 내부에 저장하고 사용하는 동작으로, 변수의 스코프를 넓게 설정하게 되면 생기는 대표적인 위험 요소입니다.

 

에라토스테네스의 체(소수를 구하는 알고리즘) 구현으로 파악하면 아래와 같습니다.

 

 

  • 2부터 시작하는 숫자 리스트(=(2..100) 등)를 만든다.
  • 첫 번째 요소를 선택한다. 이는 소수이다.
  • 남아있는 숫자 중에서 2번에서 선택한 소수로 나눌 수 있는 모든 숫자를 제거한다.
// 간단한 구현
var numbers = (2..100).toList()
val primes = mutableListOf<Int>()
while (numbers.isNotEmpty()) {
    val prime = numbers.first()
    primes.add(prime)
    numbers = numbers.filter { it % prime != 0 }
}
print(primes) // [2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97]

// 최적화: 시퀀스 활용
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 }
    }
}
print(primes.take(10).toList()) // [2,3,5,7,11,13,17,19,23,29]

 

시퀀스를 활용한 예에서 prime을 var로 선언하고, 반복문 내부에서 생성하는 것이 아니라, 반복문에 진입하기 전에 한번만 생성하는 형태로 코드를 작성하면 실행 결과가 달라집니다.

 

// 잘못된 최적화
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 }
    }
}
print(primes.take(10).toList()) // [2,3,5,6,7,8,9,10,11,12]

 

이러한 결과가 나온 이유는 prime이라는 변수를 캡처했기 때문입니다.

시퀀스를 활용하므로 필터링이 지연되어, 최종적인 prime 값으로만 필터링이 됩니다.
prime이 2로 설정되어 있을 때 필터링된 4를 제외하면, drop만 동작하므로 그냥 연속된 숫자가 나와 버립니다.

 

 

 


정리

  • 항상 잠재적인 캡처 문제를 주의하자.
  • 여러 가지 이유로 변수의 스코프는 좁게 만들어서 활용하는 것이 좋다.
  • var 보다는 val을 사용하는 것이 좋다.
  • 람다에서 변수를 캡처한다. 간단한 규칙만 지켜주면, 발생할 수 있는 여러 문제를 차단할 수 있다.

 

 

반응형