Java나 Kotlin은 가비지 컬렉터가 객체 해제와 관련된 모든 작업을 해줍니다. 하지만 그렇다고 메모리 관리를 완전히 무시해 보리면, 메모리 누수(불필요한 메모리 소비)가 발생해서, 상황에 따라 OutOfMemeoryError가 발생하기도 합니다. 따라서 더 이상 사용하지 않는 객체의 레퍼런스를 유지하면 안된다는 규칙 정도는 지켜주는 것이 좋습니다.
(특히 메모리를 많이 차지 하거나 많이 생성될 경우에는 꼭 지켜주는 것이 좋습니다.)
안드로이드를 처음 시작하는 많은 개발자가 흔히 실수로, Activity를 여러 곳에서 자유롭게 접근하기 위해서 companion(Java나 C++의 static 필드) 프로퍼티에 이를 할당해 두는 경우가 있습니다.
class MainActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// ...
activity = this
}
// ...
companion object {
// 이렇게 하지 마세요. 메모리 누수가 크게 발생합니다.
var activity: MainActivity? = null
}
}
이렇게 객체에 대한 참조를 companion(또는 static)으로 유지해 버리면, 가비지 컬렉터가 해당 객체에 대한 메모리를 해제할 수 없습니다.
메모리 문제는 굉장히 미묘한 곳에서 발생하는 경우가 많습니다. 다음과 같은 간단한 스택 구현을 봅시다.
class Statck {
private var element: Array<Any?> =
arrayOfNulls(DEFAULT_INITIAL_CAPACITY)
private var size = 0
fun push(e: Any) {
ensureCapacity()
elements[size++] = e
}
fun pop() {
if (size == 0) {
throw EmptyStackException()
}
return elements[--size]
}
private fun ensureCapacity() {
if (elements.size == size) {
elements = elements.copyOf(2 * size + 1)
}
}
companion oject {
private const val DEFAULT_INITIAL_CAPACITY = 16
}
}
위 코드는 언뜻 보기에는 문제가 없어보이지만, 문제는 pop을 할 때 size를 감소시키기만 하고, 배열 위의 요소를 해제하는 부분이 없다는 것입니다. 스택에 1000개의 요소가 있다고 가정하고 이어서 pop을 실행해서 size를 1까지 줄였다면 요소 1개만 의미가 있고 나머지는 의미가 없습니다. 하지만 위 코드의 스택은 1000개의 요소를 모두 붙들고 놓아 주지 않으므로, 가비지 컬렉터가 이를 해제하지 못합니다. 따라서 아래 처럼 객체를 더 이상 사용하지 않을 때, 그 레퍼런스에 null을 설정하기만 하면 됩니다.
fun pop(): Any? {
if (size == 0) {
throw EmptyStackException()
}
val elem = elements[--size]
elements[size] = null
return elem
}
위와 같은 예 말고도 사용되지 않는 객체는 null로 설정하는 것이 좋습니다. 특히 많은 변수를 캡쳐할 수 있는 함수 타입, Any, 제네릭 타입과 같은 미지의 클래스일 때는 이러한 처리가 중요합니다.
일반적인 규칙은 상태를 유지할 때는 메모리 관리를 염두에 두어야 한다는 것입니다. 코드를 작성할 때는 '메모리와 성능'뿐만 아니라 '가독성과 확장성'을 항상 고려해야 합니다. 일반적으로 가독성이 좋은 코드는 메모리와 성능적으로도 좋을 수 있습니다. 가독성이 좋지 않은 코드는 메모리와 CPU 리소스의 낭비를 숨기고 있을 가능성이 높습니다. (물론 둘 사이에 트레이드 오프가 발생하는 경우도 있습니다.)
일반적으로 절대 사용되지 않는 객체를 캐시해서 저장하는 경우 메모리 누수가 발생할 수 있습니다. 캐시를 사용하는 것은 좋지만 OutOfMemoryError를 일으킬 수 있다면 의미가 없습니다. 따라서 소프트 레퍼런스를 사용하여, 메모리가 부족한 경우에는 이를 알아서 해제할 수 있게 해야합니다.
사실 객체를 수동으로 해제해야 하는 경우는 굉장히 드뭅니다. 일반적으로 스코프를 벗어나면서 어떤 객체를 가리키는 레퍼런스가 제거될 대 객체가 자동으로 해제됩니다. 따라서 메모리와 관련된 문제를 피하는 가장 좋은 방법은 변수를 지역 스코프에 정의하고, Top Level 프로퍼티 또는 객체 선언(companion 객체)으로 큰 데이터를 저장하지 않는 것입니다.
'Kotlin' 카테고리의 다른 글
[Effective Kotlin] 51 - 성능이 중요한 부분에는 기본 자료형 배열을 사용하라 (1) | 2024.01.14 |
---|---|
[Effective Kotlin] 50 - 컬렉션 처리 단계 수를 제한하라 (1) | 2024.01.07 |
[Effective Kotlin] 47 - 인라인 클래스의 사용을 고려하라 (1) | 2023.12.18 |
[Effective Kotlin] 46 - 함수 타입 파라미터를 갖는 함수에 inline 한정자를 붙여라 (1) | 2023.12.10 |
[Effective Kotlin] 45 - 불필요한 객체 생성을 피하라 (1) | 2023.12.04 |