아이템 45 - 불필요한 객체 생성을 피하라
객체 생성은 언제나 비용이 들어갑니다.
상황에 따라 굉장히 큰 비용이 들어갈 수도 있습니다.
따라서 불필요한 객체 생성을 피하는 것이 최적화의 관점에서 좋습니다
JVM 에서는 하나의 가상 머신에서 동일한 문자열을 처리하는 코드가 여러 개 있다면, 기존의 문자열을 재사용합니다.
문자열 상수의 재사용 (String Pool)
JVM에서 문자열은 불변(immutable) 객체입니다. 즉, 한 번 생성된 문자열은 변경할 수 없습니다. JVM은 이러한 문자열의 특성을 활용하여 String Pool이라는 메모리 공간을 사용합니다.
String Pool이란?
String Pool은 JVM이 관리하는 특별한 메모리 영역으로, 동일한 문자열 리터럴이 여러 번 생성되는 것을 방지하고 메모리를 절약하기 위해 사용됩니다.만약 코드에서 동일한 문자열 리터럴이 여러 번 사용된다면, JVM은 String Pool에서 해당 문자열이 이미 존재하는지 확인하고, 존재하면 새로운 객체를 생성하지 않고 기존의 문자열 객체를 재사용합니다.
예전에 적었던 내용이 있어서 참고차 남김
String vs StringBuffer vs StringBuilder
String은 클래스이며, 특별한 참조 자료형 입니다. 클래스이지만 일반적으로 쓰이는 리터럴 형식으로 생성이되기도 하고, 생성자를 이용해서 생성할 수도 있습니다.생성자 방식으로 생성된 데이터는 heap메모리에 저장이됩니다.
일반적인 리터럴 형식으로 살펴보자면, 객체가 생성되면 메모리 영역이 String Constant Poll이라는 공간에 저장되고 이렇게 사용하는 것은 String Interning 이라합니다. String Interning에 저장된 String값은 불변성을 가지게 됩니다. String은 "+" 연사자를 통해 문자열을 이어 붙인다면 기존에 저장된 메모리 주소에 연결되는 것이 아니라 새로운 메모리 공간에 할당되고 GC에 의해 사라집니다. 이러한 메모리 관리 측면에서 비효율적이여서 String Constant Poll 이라는 저장공간을 사용합니다.그리고 String Constant Poll은 기본적으로 HashTable 구조를 가지고 있어서 좋은 성능을 발휘합니다.
스트링은 빈번하게 발생하는 연산시 안좋은 성능을 가지고 있고 메모리측면에서도 안좋기 때문에 이를 해결하기 위해 나온것이 StringBuffer, StringBuilder 이다.
이 둘은 가변성을 가지고 있으며, 동일 객체 내에서 문자열을 변경하는 것이 가능합니다.
하지만 두 클래스는 동기화 지원의 유무가 다르며, StringBuffer는 각 메서드 별로 Synchronized키워드를 제공하여 스레드 세이프하며, StringBuilder는 단일 쓰레드 사용 목적으로 만들어졌으며 StringBuffer보다 속도가 빠릅니다.
왜 불필요한 객체 생성을 피해야 하는가?
- 메모리 사용 증가:
- 객체를 생성할 때마다 힙 메모리에 공간이 할당됩니다. 불필요한 객체를 자주 생성하면 메모리 사용량이 증가하고, 결국 가비지 컬렉션(Garbage Collection) 주기가 빨라져 시스템 성능에 부정적인 영향을 미칩니다.
- 가비지 컬렉션 오버헤드:
- 불필요한 객체가 많아지면 가비지 컬렉터가 이를 수집하기 위해 더 자주 실행됩니다. 이는 애플리케이션의 응답성을 떨어뜨리고, CPU 사용률을 증가시킬 수 있습니다.
- 성능 저하:
- 객체 생성은 비용이 높은 작업입니다. 불필요한 객체 생성을 반복하면 성능이 저하될 수 있으며, 특히 대규모 시스템에서는 이런 성능 저하가 누적되어 문제가 될 수 있습니다.
불필요한 객체 생성을 피하는 방법
- 불변 객체 사용:
- 불변 객체(immutable object)를 사용하면 동일한 객체를 여러 곳에서 안전하게 재사용할 수 있습니다. 예를 들어, String, Int 같은 불변 객체는 새로운 객체를 생성하지 않고 재사용할 수 있습니다.
- 코틀린에서는 data class를 이용해 불변 객체를 쉽게 정의할 수 있습니다.
- 객체 캐싱:
- 자주 사용되는 객체를 캐싱하여 재사용할 수 있습니다. 예를 들어, Boolean 값이나 특정 범위의 숫자는 캐싱하여 불필요한 객체 생성을 줄일 수 있습니다.
- 객체를 매번 생성하는 대신, 필요한 객체를 캐시에 저장하고 재사용하는 방식으로 성능을 최적화할 수 있습니다.
- 팩토리 메서드 패턴 사용:
- 객체 생성 로직을 캡슐화하는 팩토리 메서드 패턴을 사용하면, 객체를 효율적으로 관리할 수 있습니다. 팩토리 메서드는 필요에 따라 객체를 생성하거나 캐시된 객체를 반환할 수 있습니다.
- 예를 들어, 동일한 설정을 가진 객체를 매번 새로 생성하지 않고, 팩토리 메서드를 통해 생성된 객체를 재사용할 수 있습니다.
- 컬렉션 객체의 재사용:
- 불필요한 컬렉션 객체의 생성을 피하기 위해, 가능한 한 컬렉션을 재사용하거나 빈 컬렉션을 미리 정의해두고 사용합니다. 코틀린에서는 emptyList(), emptySet(), emptyMap() 등의 함수를 사용해 빈 컬렉션을 재사용할 수 있습니다.
- 또한, 대규모 컬렉션이 필요하지 않은 경우 lazy 초기화나 sequence를 사용해 지연 초기화를 활용할 수 있습니다.
- 상수 객체 사용:
- 코드에서 자주 사용되는 값을 상수로 정의하여 불필요한 객체 생성을 줄입니다. 상수는 메모리에 한 번만 로드되며, 재사용할 수 있습니다.
// 불필요한 객체 생성
val list = listOf(1, 2, 3)
val newList = list.map { it * 2 }.filter { it > 2 }
// 개선된 방식: 시퀀스를 사용하여 불필요한 중간 컬렉션 생성 방지
val optimizedList = list.asSequence()
.map { it * 2 }
.filter { it > 2 }
.toList()
- Strong Reference:
- 객체가 강한 참조를 가지고 있는 한, GC는 이 객체를 수집하지 않습니다.
- 가장 일반적인 참조 형태이며, 모든 객체는 기본적으로 강한 참조를 가집니다.
- WeakReference:
- 약한 참조만 남아있을 때 GC가 객체를 수집할 수 있습니다.
- 주로 캐시에서 메모리를 효율적으로 관리하기 위해 사용됩니다.
- SoftReference:
- 메모리가 부족할 때만 GC가 객체를 수집합니다.
- 메모리 효율성을 위해 캐시에서 사용될 수 있지만, 약한 참조보다는 더 오래 객체를 유지할 수 있습니다.
무거운 객체를 외부 스코프로 보내기
- 정규표현식 객체(Regex) 함수 내부에 정의하면 함수가 호출될 때마다 Regex 객체가ㅓ 생성된다. 정규표현식은 비교적 무거운 객체이므로, 함수 외부 톱레벨로 옮겨 재사용하는 것이 성능상 유리하다.
// 내부에서 객체 생성
fun isValidIpAddress(ip: String): Boolean {
val regex = """\b(?:\d{1,3}\.){3}\d{1,3}\b""".toRegex()
return regex.matches(ip)
}
// 외부에서 객체 생성
private val ipRegex = """\b(?:\d{1,3}\.){3}\d{1,3}\b""".toRegex()
fun isValidIpAddress(ip: String): Boolean {
return ipRegex.matches(ip)
}
결론
- 일반적으로 객체를 생성하면 강한 레퍼런스(Strong Reference)로 관리됩니다.
- WeakReference는 GC가 객체를 더 쉽게 수집할 수 있게 하여, 메모리 누수를 방지하거나 캐시에서 효율적으로 사용될 수 있습니다.
- SoftReference는 메모리 부족 시에만 객체가 수집되도록 하여, 메모리 사용을 최적화하면서도 가능한 오래 객체를 유지할 수 있도록 돕습니다.
- 기본자료형을 사용하자