본문 바로가기
공부/이펙티브코틀린

아이템 2 - 변수의 스코프를 최소화하라

by 띵커베르 2024. 3. 25.
728x90
  • 상태를 정의할 때는 변수와 프로퍼티의 스코프를 최소화하는 것이 좋습니다.
    • 프로퍼티: 클래스의 일부로 객체의 상태를 나타낸다. 인스턴스 변수, 스태틱 변수
  • 프로퍼티보다는 지역 변수를 사용하는 것이 좋습니다.
  • 최대한 좁은 스코프를 갖게 변수를 사용합니다.
  • 프로그램을 추적하고 관리하기 쉽기 때문.
  • 변수는 읽기 전용 또는 읽고 쓰기 전용 여부와 상관 없이, 변수를 정의할 때 초기화되는 것이 좋습니다.
  • 여러 프로퍼티를 한꺼번에 설정해야 하는 경우에는 구조분해 선언(destructuring declarration)을 화용하는 것이 좋습니다.
    • 구조분해: 객체가 가지고 있는 여러 값을 분해해서 여러 변수를 한꺼번에 초기화 할 수 있다.

// 책에 있는 예시도 좋치만 좀 더 간단한 예시
// data class 는 구조분해 선언을 자동으로 지원하기 때문에 별도의 구현 없이도 사용할 수 있다.
// js 에서도 비슷한 문법을 사용 중

data class Person(val name: String, val age: Int, val city: String)

@Test
fun `구조 분해선언 좀 더 간단한 예시`() {
    val person = Person("Injin Jeong", 17, "Seoul")

    // 구조분해 선언을 사용하여 Person 객체의 프로퍼티를 변수에 할당
    val (name, age, city) = person

    println("$name, $age years old, from $city") // Injin Jeong, 17 years old, from Seoul
}

========================================
class 일때는 어떻게..??? -> componentN 함수를 구현해 줘야 한다.
- 이렇게 사용할 일이 얼마나 있을까 싶지만..재미로 보장~
class PersonClass(val name: String, val age: Int, val city: String) {
    operator fun component1() = name
    operator fun component2() = age
    operator fun component3() = city
}

@Test
fun `구조분해 class 일때`() {
    val person = PersonClass("Injin Jeong", 17, "Seoul")
    val (name, age, city) = person
    println("$name, $age years old, from $city") // Injin Jeong, 17 years old, from Seoul
}

 


 

  • 캡처링
    • 재미로보는 에라토스체네의 체 (소수는 약수로 1과 자기자신만을 가지는 수)

@Test
fun `에라토스체네의 체`() {
    val n = 100000000
    val time = measureTimeMillis {
        val primes = BooleanArray(n + 1) { true }
        primes[0] = false
        primes[1] = false

        val sqrtN = kotlin.math.sqrt(n.toDouble()).toInt()
        for (i in 2..sqrtN) {
            if (primes[i]) {
                for (j in i * 2..n step i) {
                    primes[j] = false
                }
            }
        }

        val toList = primes.asSequence().mapIndexedNotNull { index, isPrime ->
            if (isPrime) index else null
        }.toList()
//            println(toList)
    }

    println("time: $time")
}

@Test
fun `에라토스체네의 체2`() {
    val n = 100000000
    val time = measureTimeMillis {
        val primes = BooleanArray(n + 1) { true }
        primes[0] = false
        primes[1] = false

        val sqrtN = kotlin.math.sqrt(n.toDouble()).toInt()
        for (i in 2..sqrtN) {
            if (primes[i]) {
                for (j in i * 2..n step i) {
                    primes[j] = false
                }
            }
        }

        val toList = primes.withIndex()
            .filter { it.value }
            .map { it.index }
            .toList()
//            println(toList)
    }

    println("time: $time")
}
    
@Test
fun `에라토스체네의 체3 - bit`() {
    val n = 100000000
    val time = measureTimeMillis {
        val prime = BitSet(n + 1)
        prime.set(2, n + 1)
        val sqrtN = kotlin.math.sqrt(n.toDouble()).toInt()

        for (i in 2..sqrtN) {
            if (prime[i]) {
                for (j in i * i..n step i) {
                    prime.clear(j)
                }
            }
        }

        val primesList = mutableListOf<Int>()
        for (i in 2..n) {
            if (prime[i]) {
                primesList.add(i)
            }
        }
//            println("primesList: $primesList")
    }

    println("time: $time")
}

큰수는 좀 더 고급 알고리즘으로 넘어가야한다..힙 메모리 부족으로....

  • sequence: 지연 계산을 사용하여 요소들을 순차적으로 처리할 수 있는 인터페이스
  • asSequence: 이미 존재하는 컬렉션이나 배열등의 자료구조를 sequence 로 변환하는 확장 함수

 

  • 캡처링
    • 클로저(익명 함수나 람다 식 등)가 자신이 정의된 범위 바깥의 변수를 사용할 때, 해당 변수를 "캡처"하여 사용하는 것을 말합니다. 캡처링은 클로저가 실행될 때 외부 변수의 현재 값에 접근하거나 수정할 수 있게 해줍니다. 이는 프로그래밍에서 매우 유용하지만, 때로는 예상치 못한 결과나 성능 문제를 일으킬 수 있습니다.
    • 스마트 캡처링

@Test
fun `캡처링 문제`() {
    val time = measureTimeMillis {
        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())
    }
    println("time: $time")
}

// [2, 3, 5, 6, 7, 8, 9, 10, 11, 12]
  • 이러한 결과가 나온 이유는 prime 이라는 변수를 캡처했기 때문이다. 반복문 내부에서 filter 를 활용해서 prime 으로 나눌 수 있는 숫자를 필터링합니다. 그런데 시퀀스를 활용하므로 필터링이 지연됩니다. 따라서 최종적인 prime 값으로만 필터링된 것입니다. prime 이 2 로 설정되어 있을 때 필터링된 4를 제외하면, drop 만 동작하므로 그냥 연속된 숫자가 나와 버립니다.
더보기

시퀀스로 인하여 실제로 값이 필요할때까지 실행되지 않습니다.

시퀀스 연산이 수행되고, 해당 시점에 prime 변수의 값을 사용하여 필터링을 수행합니다.

 

문제는 -> filter 가 호출될 때마다 prime 변수의 최신값(바깥 루프에서 업데이트된 값)을 사용한다는 것.

filter 는 prime 이 변경될 때마다 이 변경을 캡처하여 이후의 모든 필터링 연산에 최종적으로 캡처된 prime 값(루프의 마지막에서 사용된 값)을 사용하게 됩니다.

 

좀더 자세히..->

 

첫 번째 반복:
- numbers 시퀀스는 2부터 시작합니다.
- prime은 numbers.first()로부터 2를 얻습니다.
- yield(prime)을 통해 첫 번째 소수인 2를 반환합니다.
- 이제 numbers는 2를 제외하고 3부터 시작하는 시퀀스로 업데이트되며, 2로 나누어 떨어지지 않는 숫자들만 필터링하도록 설정됩니다. 즉, 이 시점에서 numbers는 3, 4, 5, 6, ...이 됩니다.


두 번째 반복:
- prime은 이제 3을 얻습니다.
- 3을 yield합니다.
- numbers는 이제 3으로 나누어 떨어지지 않는 숫자만 포함해야 합니다. 하지만 실제 필터링 작업은 지연되기 때문에, 아직 적용되지 않았습니다.
- 여기까지는 문제가 없어 보이지만, 실제로 filter가 적용되는 순간은 primes.take(10).toList()가 호출될 때입니다. 이 때까지 prime 변수는 마지막으로 발견된 소수 값으로 계속 업데이트됩니다.

 

primes.take(10).toList() 호출 시:
- 이제 filter가 실제로 실행됩니다. 하지만 모든 filter 호출은 마지막에 캡처된 prime 값으로 필터링을 시도합니다. 만약 마지막 prime이 11이었다면, 모든 숫자를 11로 나누어 떨어지지 않는지 검사합니다. 이는 전혀 의도한 바가 아닙니다.
결과적으로, 이전 단계에서 의도했던 소수에 의한 필터링(예: 2로 나누어 떨어지지 않는 숫자, 3으로 나누어 떨어지지 않는 숫자 등)이 제대로 이루어지지 않았습니다.

 

 


  • 여러가지 이류로 변수의 스코프는 좁게 만들어서 활용하는 것이 좋다.
  • 또한 var 보다는 val 을 사용하는 것이 좋습니다.
  • 람다에서 변수를 캡처한다는 것을 꼭 기억하자.
728x90

댓글