아이템 45 - 불필요한 객체 생성을 피하라
객체 생성은 언제나 비용이 들어가며, 상황에 따라 큰 비용이 발생할 수 있으므로 불필요한 객체 생성을 피하는 것이 좋습니다.
JVM의 객체 생성 최적화
동일 문자열일 때, 기존 문자열 재사용
JVM에서는 하나의 가상 머신에서 동일한 문자열을 처리하는 코드가 여러개가 있다면, 기존 문자열을 재사용합니다.
Integer
나 Long
와 같은 박스화한 기본 자료형에 대해서도 작은 값에 대해서는 재사용
Kotlin/JVM에서 Int
나 Double
은 자바에서와 다르게 원시 타입입니다. 박싱 타입일 때, 작은 값들에 대해서는 캐싱을 하여 해당 값이 필요할 때 매번 새로운 객체를 생성하지 않고 기존 객체를 활용할 수 있습니다.
언제 박싱 타입으로 동작할까?
nullable
하게 타입을 설정한 경우(예: Int?)- 타입을 제네릭으로 사용하는 경우
기본 자료형은null
이 될 수 없고, 타입 아규먼트(Generic
타입에 명시된 타입)로 사용할 수 없기 때문에nullable
이거나 타입 아규먼트로 사용될 경우 박싱 타입으로 객체가 생성됩니다.
코틀린 자료형 | 자바 자료형 |
Int | int |
Int? | Integer |
List<Int> | List<Integer> |
위 문서에는 Integer
에 대해서만 나와있지만, Long
타입으로 코틀린에서 테스트했을 때도 Integer
와 동일한 범위로 캐싱하는 것을 확인할 수 있었습니다.
객체 생성 비용은 항상 클까?
어떠한 객체를 wrap하면 크게 세 가지 비용이 발생합니다.
- 객체는 더 많은 용량을 차지합니다.
- 기본 자료형인
int
는 4바이트이지만,Integer
로 wrap하여 사용하면 16바이트이며, 이에 대한 레퍼런스로 인해 8바이트를 더 필요하기 때문에 5배 이상 용량을 차지합니다.
- 기본 자료형인
- 요소가 캡슐화되어 있다면, 접근에 추가적인 함수 호출이 필요합니다.
- 함수 처리는 빠르기 때문에, 큰 비용은 아니지만 수많은 객체를 처리한다면 비용이 커질 수 있습니다.
- 객체는 생성되어야 합니다.
- 객체가 생성되고 메모리에 할당되고, 레퍼런스를 만드는 등 추가 작업이 필요합니다.
객체 재사용 방법
객체 선언
매번 객체를 생성하지 않고 재사용하기 위해 객체 선언을 사용(싱글톤)해볼 수 있습니다.
sealed class LinkedList<out T>
class Node<out T>(
val head: T,
val tail: LinkedList<T>
): LinkedList<T>()
object Empty: LinkedList<Nothing>()
// 사용 예
val list: LinkedList<Int> = Node(1, Node(2, Node(3, Empty)))
val list2: LinkedList<String> = Node("A", Node("B", Empty))
- 마지막 노드의 tail이 비어있어야 할 때 사용하는
Empty
를 각 타입별로 만들지 않고 싱글톤 객체로 생성하여 객체를 재사용하는 방법입니다. - 싱글톤으로 만들때 모든 타입의 서브타입인
Nothing
을 사용하고,Node
의 제네릭 타입에out
한정자를 붙여서 어떤 타입이던 올 수 있게 한 것입니다.
위와 같은 객체 선언은 immutable sealed
클래스를 정의할 때 자주 사용되며, mutable
객체는 공유 상태 관리와 관련된 버그를 찾기 어려울 수 있으므로 불변성일 때 사용합니다.
캐시를 활용하는 팩토리 함수
객체를 만들 때 팩토리 메소드를 사용해 만드는 경우가 있고, 팩토리 함수는 캐시(cache)를 가질 수 있어 항상 같은 객체를 리턴하게 할 수 있습니다. 실제로 stdlib의 emptyList
는 이를 활용하여 구현되어 있습니다.
val listTest: List<String> = emptyList()
/**
* Returns an empty read-only list. The returned list is serializable (JVM).
* @sample samples.collections.Collections.Lists.emptyReadOnlyList
*/
public fun <T> emptyList(): List<T> = EmptyList
위처럼 emptyList()
함수를 호출하게 되면, 아래와 같은 EmptyList
객체를 반환하게 되어 있습니다.
위와 같은 케이스 이외에도 parameterized
팩토리 메소드도 캐싱을 활용할 수 있습니다. 예를 들어 객체를 아래와 같이 map에 저장해 둘 수 있습니다.
private val connections = mutableMapOf<String, Connection>()
fun getConnection(host: String) = connections.getOrPut(host) { createConnection(host) }
이렇게 캐시를 위해 map에 저장할 경우, 메모리를 많이 사용할 수도 있다는 단점이 있습니다. 만약 메모리 문제가 생긴다면, 메모리를 해제하면 되며, GC가 메모리 필요 시에만 자동으로 해제할 수 있도록 Soft Reference
를 사용하면 좋습니다.
- Weak Reference
- 가비지 컬렉터가 값을 정리하는 것을 막지 않습니다.
- 따라서, 다른 레퍼런스(변수)가 이를 사용하지 않으면 곧바로 제거됩니다.
- Soft Reference
- 가비지 컬렉터가 값을 정리할 수도 있고, 정리하지 않을 수도 있습니다. 메모리가 부족해서 추가로 필요한 경우에만 정리합니다.
- JVM 구현마다 다를 순 있지만, 모든
Soft references
는 JVM이 OOM 에러를 발생시키기 전에 메모리 정리하는 것을 보장합니다. - 따라서, 캐시를 만들 때에는
SoftReference
를 사용하는 것이 좋습니다.
무거운 객체를 외부 스코프로 보내기
성능을 위한 유용한 트릭으로, 무거운 객체를 외부 스코프로 보내는 방법이 있습니다. 컬렉션 처리에서 이루어지는 무거운 연산은 컬렉션 처리 함수 외부로 빼는 것이 좋습니다.
예를 들어, Iterable
내 최댓값의 수를 세는 확장함수가 있고, Iterable
내부에서 최댓값을 찾는 작업이 무거운 연산이라고 가정합니다.
// 컬렉션 처리 함수 내부에서 무거운 연산
fun <T: Comparable<T>> Iterable<T>.countMax(): Int = count { it == this.max() }
// 컬렉션 처리 함수 외부에서 무거운 연산
fun <T: Comparable<T>> Iterable<T>.countMax(): Int {
val max = this.max()
return count { it == max }
}
이때, 함수는 위 방법 중 하나로 만들어집니다. 컬렉션 처리 함수 외부에서 무거운 연산을 진행할 경우 아래와 같은 장점이 있습니다.
- 처음에
max
값을 찾아 변수에 보관함에 따라, 가독성이 향상됩니다. max
값을 한 번만 찾기 때문에 코드의 성능이 좋아집니다.
연산을 외부로 추출해서 값 계산을 추가로 하지 않게 만드는 작업은, 당연하게 느껴질 수 있지만 개발을 하다보면 자주 실수하기 좋은 부분입니다.
지연 초기화
무거운 클래스를 만들 때는 아래와 같이 내부 인스턴스들을 지연 초기화할 수 있습니다.
class A {
val b = B()
val c = C()
val d = D()
// ...
}
// 내부 인스턴스들을 지연 초기화
class A {
val b by lazy { B() }
val c by lazy { C() }
val d by lazy { D() }
// ...
}
지연 초기화는 장점도 있지만 단점도 있기 때문에, 상황에 맞게 사용해야 합니다.
- 장점
- 객체 생성하는 과정이 가벼워집니다.
- 단점
- 만약, 지연 초기화되는 객체를 이용해야 하는 함수의 속도가 매우 빨라야 한다면, 해당 함수를 처음 호출할 때 무거운 객체 초기화 작업이 필요하여 호출 응답 속도가 낮아질 수 있습니다. (빽엔드에서 좋지 않을 수 있음)
기본 자료형 사용하기
JVM은 숫자와 문자 등의 기본적인 요소를 나타내기 위한 특별한 기본 내장 자료형을 가지고 있으며, 이를 기본 자료형(primitives)라고 합니다. Kotlin/JVM 컴파일러는 내부적으로 이러한 기본 자료형을 최대한 사용하도록 구현되어 있습니다.
숫자와 관련된 연산 정도는 어떤 형태의 자료형(기본 자료형 또는 wrap한 자료형)을 사용하던 성능적으로 큰 차이는 없으나, 큰 컬렉션을 처리할 때는 성능 차이를 확인할 수 있습니다.
예를 들어, 코틀린으로 컬렉션 내부의 최댓값을 리턴하는 함수는 어떻게 구현되느냐에 따라 성능 차이가 있습니다.
fun Iterable<Int>.maxOrNull(): Int? {
var max: Int? = null
for (i in this) {
max = if(i > (max ?: Int.MIN_VALUE)) i else max
}
return max
}
위처럼 구현할 경우, 두 가지 단점이 있습니다.
- 각각의 단계에서 엘비스(Elvis) 연산자를 사용해야 합니다.
nullable
값을 사용했기 때문에 JVM 내부에서int
가 아닌Integer
로 연산이 이루어집니다.
위 단점을 해결하기 위해서는 아래와 같이 while
반복문을 사용해 구현할 수 있습니다.
fun Iterable<Int>.maxOrNull(): Int? {
val iterator = iterator()
if (!iterator.hasNext()) return null
var max: Int = iterator.next()
while (iterator.hasNext()) {
val e = iterator.next()
if (max < e) max = e
}
return max
}
아래는 위 예시를 10,000,000개 원소를 가진 케이스에 대입한 것으로, 기본 자료형을 사용하는 것이 훨씬 빠름을 확인할 수 있습니다.
컬렉션 내부에 원소가 몇개 없다면, 굳이 wrap한 자료형을 못 쓸 이유는 없지만, 많은 원소가 컬렉션에 포함될 수 있다면 최대한 기본 자료형을 사용하도록 로직을 개선하는 것이 좋습니다. 외부에 제공할 라이브러리로써 함수를 만들었다면, 원소가 얼마나 들어올지 제어할 수 없기 때문에 더더욱 성능을 생각하고 구현해야 합니다.
아이템 46 - 함수 타입 파라미터를 갖는 함수에 inline 한정자를 붙여라
코틀린 표준 라이브러리의 고차 함수(함수를 파라미터로 받는 함수 또는 함수를 리턴하는 함수)를 보면, 대부분 inline
한정자가 붙어있습니다. inline
한정자의 역할은 컴파일 시점에 함수를 호출하는 부분을 함수의 본문으로 대체하는 것입니다.
// 아래와 같이 코드를 작성한다면
repeat(5) {
print(it)
}
// 아래처럼 컴파일 시점에 대체됨
for (index in 0 until 5) {
print(it)
}
일반적인 함수 호출 시, 함수 본문으로 점프하고 해당 함수를 모두 완료되면 다시 해당 함수 호출했던 위치로 점프하는 과정을 거칩니다. 하지만, inline
함수를 호출하면 함수의 본문으로 함수 내용이 대체되어서 점프가 일어나지 않습니다.
inline
한정자 사용할 때의 장점은 아래와 같으며, inline
한정자의 장점과 단점에 대해 하나씩 알아보도록 하겠습니다.
- 타입 아규먼트에
reified
한정자를 붙여 사용할 수 있습니다. - 함수 타입 파라미터를 가진 함수가 훨씬 빠르게 동작합니다.
- 비지역(non-local) 리턴을 사용할 수 있습니다.
inline
한정자 장점
타입 아규먼트를 reified
로 사용할 수 있음
J2SE 5.0 버전 자바 전까지는 제네릭이 존재하지 않았습니다. 따라서, 하위 버전 호환을 위해 제네릭 사용 후 컴파일을 하면 제네릭 타입과 관련된 내용이 제거됩니다. 예를 들어 List
<Int
>를 컴파일하면 List
로 바뀝니다. 따라서, 로직에서 객체가 List
인지 확인하는 코드는 작성이 가능하지만, List
<Int
>인지 확인하는 코드는 사용할 수 없습니다.
함수를 인라인으로 만들고, refied
한정자를 사용하면 위와 같은 제한을 무시할 수 있습니다.
- 함수 호출이 함수 본문으로 대체됨에 따라
reified
한정자 지정을 통해 타입 파라미터를 사용한 부분이 타입 아규먼트로 대체됩니다.
함수 타입 파라미터를 가진 함수가 더 빠르게 동작함
모든 함수는 inline
한정자를 붙이면 더 빠르게 동작합니다. 아무래도, 호출된 함수로 점프했다가 다시 호출부로 점프하는 행위가 없기 때문에 빠를 수 밖에 없습니다. 그래서, 표준 라이브러리에 있는 간단한 함수들 대부분에는 inline
한정자가 붙어 있습니다.
하지만, 함수 파라미터를 가지지 않는 함수에는 큰 성능 차이를 발생시키지 않으며, 함수 파라미터가 없는 간단한 함수에 inline
을 붙이면 intelliJ는 경고를 표시해줍니다.
inline
한정자 유무에 따라 함수 내에서 지역 변수를 캡처할 때 성능 차이를 보입니다. inline
이 아닌 람다 표현식에서는 지역 변수를 직접 사용할 수 없기 때문에, 위 예시처럼 함수가 inline
이 아니라면 지역 변수를 레퍼런스 객체로 래핑하고 람다 표현식 내부에서 사용해야 합니다.
실제로 위 예시를 테스트해보면 아래와 같이 수행 시간의 차이를 확인할 수 있습니다. 다만, 이 정도로 많이 호출되지 않는 이상 성능상 큰 차이는 발생하지 않을 수 있습니다.
- 10,000,000번 수행할 때에는 1ms 차이밖에 발생하지 않았음
어떨 때inline
함수로 만들지 헷갈린다면, 함수 타입 파라미터를 사용하는 컬렉션 처리와 같은 유틸리티 함수를 만들 때inline
을 붙인다고 생각해도 괜찮습니다.
비지역적 리턴(non-local return)을 사용할 수 있음
위 예시에서 볼 수 있듯이, 일반 함수의 함수 타입 파라미터 내에서는 리턴을 사용할 수 없습니다. 이는 함수 리터럴이 컴파일 될 때, 함수가 객체로 래핑되기 때문입니다. 함수가 다른 클래스에 위치하면, return을 사용해서 main으로 돌아올 수 없기 때문에 이런 제한이 있는 것입니다.
inline
한정자 단점
inline
한정자 비용
앞서 살펴봤던 것처럼 inline
한정자는 장점을 많이 가지고 있지만, 모든 곳에 사용할 수는 없습니다. 대표적인 예로, 인라인 함수는 재귀적으로 사용할 경우 무한하게 함수본문으로 대체되는 문제가 발생할 수 있습니다. 이 경우, 인텔리제이가 오류로 잡아주지 못하기 때문에 위험합니다.
또한, 인라인 함수는 더 많은 가시성 제한을 가진 요소를 사용할 수 없습니다. 예를 들어 public
인라인 함수 내에서는 private
과 internal
가시성을 가진 함수와 프로퍼티를 사용할 수 없습니다.
crossinline
과 noinline
함수를 인라인으로 만들고 싶지만, 어떤 이유로 일부 함수 타입 파라미터는 inline
으로 받고 싶지 않을 수 있습니다. 이 경우, 아래와 같은 한정자를 사용합니다.
crossinline
코틀린 람다에서 return
을 호출하려면 레이블(예: return@레이블명
)을 통해야 합니다. 람다에서 일반 return
을 사용할 수 없는 이유는 해당 람다를 둘러싼 함수를 종료시키기 때문입니다. 아래 예시에서 볼 수 있듯이,
- 예를 들어,
main
함수에서 람다 함수를 호출하고return
문을 람다 내부에 작성한다면, 람다는main
함수를 종료시켜야 합니다. - 하지만, 해당 함수가
main
함수와 다른 클래스에 있으면main
으로 되돌아갈 수 없기 때문에 일반return
을 사용할 수 없습니다.
코틀린 컴파일러는 람다 내부에서 return
을 사용하는 것을 허용하지 않고 여기서의 return
을 non-local return(비지역적 리턴)이라고 부릅니다.
fun foo() {
val f = {
println("Hello")
return // won't compile -> non-local return이라고 부름
}
}
inline
한정자를 사용할 때에는 일반 람다와 다르게 non-local return을 사용할 수 있습니다.
- 아래 예시를 보면,
inline
에 의해 람다는main
함수 본문에 존재하기 때문에, 메인 함수가 종료되게 할 수 있습니다.
inline fun foo(f: () -> Unit) {
f()
}
fun main() {
foo {
println("Hello World")
return // 에러 발생하지 않음 -> inline 함수에서는 non-local return 가능
}
}
하지만, 아래 예시처럼 inline
함수 내에서 호출하는 함수 아규먼트가 일반 함수를 호출하도록 inline
함수를 구성한다면, 컴파일 에러가 발생합니다.
위처럼 에러가 발생하는 이유는 아래 예시처럼, inline
함수 내에 일반 함수가 있어 비지역적 리턴을 사용하지 못하는데, inline
함수의 함수 아규먼트로 비지역적 리턴을 호출하도록 로직이 구성될 수 있기 때문입니다.
이때, inline
함수의 이점을 그대로 누리면서 compile에 성공하기 위해서 아래와 같이 crossinline
한정자를 사용할 수 있습니다.
crossinline
한정자를 사용한다고 해서, 일반 함수가 inline
함수가 되는 것은 아니기 때문에, 그저 람다에서 non-local return 기능을 사용하지 못하게 제거하고 inline
함수의 효율성은 그대로 누릴 수 있도록 하는 것입니다.
crossinline
에 대해 다시 정리하면 아래와 같은 역할을 합니다.
- 아규먼트로 인라인 함수를 받지만, 비지역적 리턴을 하는 함수는 받을 수 없게 만듭니다.
- 인라인으로 만들지 않은 다른 람다 표현식과 조합해서 사용할 때 문제가 발생하는 경우 사용합니다.
noinline
noinline
한정자는 inline
한정자가 붙은 함수의 함수 아규먼트 중, 함수 본문으로 만들고 싶지 않을 경우 사용하는 한정자입니다.
inline fun executeAll(action1: () -> Unit, noinline action2: () -> Unit) {
action1()
action2()
}
fun main() {
executeAll({ print("Hello") }, { print(" World") })
}
// 위처럼 호출 시, 아래와 같이 작동함
fun main() {
print("Hello")
val action2 = { print(" World") }
action2()
}
- 아규먼트로 인라인 함수를 받을 수 없게 만듭니다.
- 인라인 함수가 아닌 함수를 아규먼트로 사용하고 싶을 때 활용합니다.
인라인 함수는 함수 타입 파라미터를 갖는 톱레벨 함수를 정의할 경우 많이 사용되며, 특히 컬렉션 처리 같은 헬퍼 함수(map
,filter
,flatMap
,joinToString
등)나 스코프 함수(also
,apply
,let
등), 톱레벨 유틸리티 함수(repeat
,run
,with
)에서 사용됩니다.
API를 정의할 때는 인라인 함수를 거의 사용하지 않으며, 인라인 함수가 다른 인라인 함수를 호출하면 코드가 기하급수적으로 늘어날 수 있으니 주의가 필요합니다.
아이템 47 - 인라인 클래스의 사용을 고려하라
도메인 특화된 타입으로 값을 감싸야 할 경우가 있습니다. 하지만, 이 경우, 클래스로 만듬에 따라 추가적인 힙 할당이 필요하기 때문에 런타임 오버헤드를 일으킬 수 있고, 감싸진 값의 유형이 기본 타입이라면 성능 저하가 상당히 클 수 있습니다. 기본 타입은 일반적으로 런타임에 의해 최적화가 이루어지는데, 기본 타입을 감쌌기 때문에 그러한 혜택을 받을 수 없기 때문입니다.
이때 사용할 수 있는 것이 Kotlin의 inline class
입니다. inline class
는 value-based class
의 하위 개념이며, 따로 ID
는 없고 값만 가질 수 있는 클래스입니다. 이는 코틀린 1.3부터 도입된 기능으로, 기본 생성자 프로퍼티가 하나인 클래스 앞에 value
을 붙이면 해당 객체를 사용하는 위치가 모두 해당 프로퍼티로 교체됩니다.
참고로, Kotlin/JVM에서 value class
를 사용할 때에는 클래스 선언부에 @JvmInline
어노테이션을 붙여주어야 하며, 인라인 클래스의 객체를 만들 경우, 컴파일 시점에 해당 객체를 사용하는 위치에 인라인 클래스의 프라퍼티로 모두 교체가 됩니다.
인라인 클래스의 특징
인라인 클래스는 일반적인 클랫의 기능을 일부 지원하며, 특히 함수나 프로퍼티를 가질 수 있고 init 블럭과 보조 생성자도 가질 수 있습니다. 또한, 인라인 클래스는 인터페이스 상속을 허용합니다. 다만, 다른 클래스를 상속할 수 없으며 인라인 클래스는 항상 final
클래스여야 합니다.
참고로, 인터페이스를 상속한 인라인 클래스를 인라인 클래스답게 사용하려면 아래 예시처럼 override
하는 메소드에 접근할 때 인터페이스 타입이 아닌, 인라인 클래스 타입으로 접근해야 합니다. 인터페이스를 통해 접근하게 되면, 객체를 래핑해서 사용해야 하기 떄문에 인라인 클래스로 동작하지 않습니다.
인라인 클래스는 다른 자료형을 래핑해서 새로운 자료형을 만들 때 많이 사용하며, 보통 아래와 같은 상황에서 사용합니다.
- 측정 단위를 표현할 때
- 타입 오용으로 발생하는 문제를 막을 때
typealias
typealias
를 사용하면, 타입에 새로운 이름을 붙여 줄 수 있어 유용하게 쓰일 수 있을 것 같습니다.
typealias ClickListener = (view: View, event: Event) -> Unit
class View {
fun addClickListener(listener: ClickListener) {}
fun removeClickListener(listener: ClickListener) {}
}
하지만, typealias
는 안전하지 않습니다. 아래 예시를 보면, Second
와 Hours
변수는 이름이 명확하게 지정되어 있어 안전할 것 같지만, 각각 Int
로 값을 서로 반대로 넣어도 오류가 나지 않습니다. 이런 상황에서는 typealias
를 사용하지 않는 것이 오히려 오류를 찾기 쉬울 수 있습니다.
typealias Seconds = Int
typealias Hours = Int
fun main() {
val seconds: Seconds = 30
val hours: Hours = seconds // 컴파일 오류나지 않음
}
Inline class
VS type alias
inline
클래스와 type alias
는 유사해보입니다. 둘 다 새로운 타입을 만들고 런타임에 기본 타입으로 표현됩니다. 이 둘간의 중요한 차이점은 type alias
는 기본 타입과 할당이 호환(assignment-compatible)되는 반면, inline class
는 호환되지 않는다는 것입니다.
즉, inline
클래스는 기존 유형에 대해 대체 이름만 생성하는 type alias
와 달리, 완전히 새로운 타입을 도입하는 것입니다.
typealias NameTypeAlias = String
@JvmInline
value class NameInlineClass(val s: String)
fun acceptString(s: String) {}
fun acceptNameTypeAlias(n: NameTypeAlias) {}
fun acceptNameInlineClass(p: NameInlineClass) {}
fun main() {
val nameAlias: NameTypeAlias = ""
val nameInlineClass: NameInlineClass = NameInlineClass("")
val string: String = ""
acceptString(nameAlias) // OK: pass alias instead of underlying type
acceptString(nameInlineClass) // Not OK: can't pass inline class instead of underlying type
// And vice versa:
acceptNameTypeAlias(string) // OK: pass underlying type instead of alias
acceptNameInlineClass(string) // Not OK: can't pass underlying type instead of inline class
}
아이템 48 - 더 이상 사용하지 않는 객체의 레퍼런스를 제거하라
자바는 가비지 컬렉터가 객체 해제와 관련된 모든 작업을 해줍니다. 하지만, 그렇다고 메모리 관리를 완전히 무시하면 메모리 누수가 발생해서 상황에 따라 OutOfMemoryError가 발생하기도 합니다. 따라서, 더 이상 사용하지 않는 객체의 레퍼런스를 유지하면 안된다는 규칙 정도는 지켜주는 것이 좋습니다.
객체에 대한 참조를 companion
(또는 static
)으로 유지하면, 가비지 컬렉터는 해당 객체에 대한 메모리 해제를 할 수 없습니다. 따라서 의존 관계를 정적으로 저장하지 않고 다른 방법을 활용해서 적절하게 관리해야 합니다. 또한 객체에 대한 레퍼런스를 다른 곳에 저장할 때는 메모리 누수가 발생할 가능성을 염두해 두어야 합니다.
쓸데없는 최적화가 모든 악의 근원이라는 말이 있긴 하지만, 오브젝트에 null을 설정하는 것은 어려운 일이 아니기 때문에 더 이상 사용하지 않는 객체에 대해 null
설정을 해주면 좋습니다.
일반적으로, 코드를 작성할 때는 메모리와 성능 뿐 아니라 가독성과 확장성을 고려해야 합니다. 보통 가독성이 좋은 코드가 메모리와 성능적으로도 좋습니다.
만약, 메모리와 성능, 가독성과 확장성 간에 트레이드 오프가 발생한다면 그 때는 보통 가독성과 확장성을 중시하는 것이 좋습니다. 다만, 예외적으로 라이브러리를 구현할 때는 메모리와 성능이 더 중요합니다.
사실 객체를 수동으로 해제해야 하는 경우는 매우 드뭅니다. 보통 스코프를 벗어나면서 객체를 가리키는 레퍼런스가 제거될 때 객체가 자동으로 해제됩니다. 따라서 메모리와 관련된 문제를 피하는 가장 좋은 방법은 아이템 2에서 배운 변수의 스코프를 최소화하라에서 언급한 것처럼 변수를 지역 스코프에 정의하고, 톱레벨 프로퍼티 또는 객체 선언으로 큰 데이터를 저장하지 않는 것입니다.
스터디 준비
궁금한 점
- 아이템 45
- 박스화한 기본 자료형에 대해 작은 값들은 캐싱하여 사용하는데, 왜 kotest에서는 해당 테스트가 통과하지 못할까?
- 관련해서 stackoverflow에 질문 남겨둠
- 박스화한 기본 자료형에 대해 작은 값들은 캐싱하여 사용하는데, 왜 kotest에서는 해당 테스트가 통과하지 못할까?
- 아이템 48
- 더 이상 사용하지 않는 객체에 null처리를 하시는지? 보통 val로 선언을 해서 null로 변경이 어렵기도 하고, 어떤 상황일 때 필요할지 감이 오지 않음
고찰
- 아이템 47
- inline value class를 사용하면 VO를 만들어볼 수 있을 것 같고, 오버헤드도 없다고 하니 실무에 적용해보면 좋을 것 같음
참고 자료
- 이펙티브 코틀린(마르친 모스칼라 저)
- Kotlin docs - Numbers representation on the JVM
- Java Language Specification, Java SE 8, 3.10.5
- baeldung - Java Soft References
- baeldung - kotlin crossinline vs noinline
- Kotlin docs - Inline value classes
'PROGRAMMING LANGUAGE > KOTLIN' 카테고리의 다른 글
[Kotlin Coroutine] channel & select (0) | 2024.08.16 |
---|---|
[Effective Kotlin] 8장 효율적인 컬렉션 처리 (0) | 2024.08.10 |
[Effective Kotlin] 6장 클래스 설계 (0) | 2024.07.28 |
[Kotlin Coroutine] 코루틴 단위 테스트 (0) | 2024.07.27 |
[Kotlin Coroutine] Asynchronous Flow (5) | 2024.07.20 |