목표
똑같은 기능의 객체를 여러번 생성하는 것 보단 객체 하나를 재사용하는 것이 나을 때가 많습니다.
이 점을 고려할 때. 피해야하는 방법과 권장되는 방법 등을 알아봅시다.
1. 문자열 생성에서의 불필요한 객체 생성 피하기
문자열을 선언하는 방법에는 String 리터럴과 new String() 두 가지가 있습니다.
"abc" 리터럴 값을 사용하여 새로운 인스턴스를 만드는 대신 하나의 String인스턴스를 사용할 수 있습니다.
반면, new String("abc")는 새로운 인스턴스를 만들기 때문에
이를 반복문이나 빈번하게 호출되는 메서드 안에서 호출하게 된다면 불필요하게 매모리를 낭비하게 될 수 있습니다.
참고: "abc" vs new String("abc") 더 알아보기
[JAVA] 리터럴 String vs String 객체
목표 문자열을 선언하는 방법에는 String 리터럴과 new String() 두 가지가 있다. 둘의 차이를 알아보자. String 리터럴 값 Java의 문자열 리터럴은 상수로 취급되며, 이러한 문자열들은 JVM 내부의 특정
je-pa.tistory.com
2. 정적 팩터리 메서드를 이용한 불필요한 객체 생성 피하기
정적 팩터리 메서드(Static Factory Method)는 객체 생성을 담당하는 정적 메서드로, 해당 클래스의 인스턴스를 반환합니다. 생성자는 호출 마다 새로운 객체를 반환하지만 팩터리 메서드는 그렇지 않습니다.
이를 사용하여 불필요한 객체 생성을 피할 수 있습니다. 여기에 몇 가지 예시를 들어보겠습니다.
Boolean 클래스
Boolean result = Boolean.valueOf(true);
위 코드에서 Boolean.valueOf()는 boolean 값을 기반으로 하여 Boolean 객체를 반환합니다.
이때, 내부적으로 캐싱된 Boolean 인스턴스들 중 동일한 값을 가진 객체가 이미 존재한다면,
새로운 객체를 생성하지 않고 기존의 인스턴스를 재사용하여 성능상 이점을 가져옵니다.
따라서 Boolean(String)생성자 대신 Boolean.valueOf(String) 팩터리 메서드를 사용하는 것이 좋습니다.
참고: JAVA9에서 Boolean(String)생성자는 사용자제 API로 지정되었다.
Calendar 클래스
Calendar calendar = Calendar.getInstance();
Calendar.getInstance()는 현재 날짜와 시간 정보를 갖는 Calendar 인스턴스를 반환합니다.
이 방법은 매번 새로운 Calendar 인스턴스를 생성하는 대신에, 재사용 가능한 단일 인스턴스에서 날짜 및 시간 정보만 업데이트하여 사용함으로써 오버헤드 없이 원하는 작업을 수행할 수 있게 해줍니다.
Collections 클래스
List<Integer> numbers = Collections.emptyList();
Set<String> strings = Collections.singleton("example");
위 코드에서 사용된 두 개의 정적 팩터리 메서드 Collections.emptyList()와 Collections.singleton()은 불변(empty) 리스트와 요소가 하나인(set with one element) 집합을 반환합니다. 이를 사용하여 비어있는 컬렉션 객체를 생성하거나, 단일 요소만 포함하는 컬렉션 객체를 생성할 수 있습니다.
Collections 코드를 확인했을 때
public class Collections {
// emptyList(): 불변(empty) 리스트 반환
public static final List EMPTY_LIST = new EmptyList<>();
public static final <T> List<T> emptyList() {
return (List<T>) EMPTY_LIST;
}
// singletonList(): 요소가 하나인(set with one element) 집합을 반환
public static <T> List<T> singletonList(T o) {
return new SingletonList<>(o);
}
private static class SingletonList<E> extends AbstractList<E> implements RandomAccess, Serializable {
private final E element;
SingletonList(E obj) {element = obj;}
}
}
Collections.singleton()은 주로 다음과 같은 상황에서 SingletonList를 사용할 수 있습니다
- 단일 요소로 구성된 리스트가 필요한 경우: 만약 리스트에 하나의 요소만 포함하고, 추가적인 변경이 없을 것으로 예상되면 SingletonList를 사용할 수 있습니다. 이렇게 하면 메모리 및 리소스 소비가 줄어들며, 코드도 간결해집니다.
- 읽기 전용 리스트 생성: SingletonList는 불변(immutable)이므로 한 번 생성된 후에 내부 요소를 수정할 수 없습니다. 따라서 읽기 전용 리스트를 생성하기 위해 사용될 수 있습니다.
다음은 SingletonList의 사용 예시입니다
import java.util.Collections;
import java.util.List;
public class Main {
public static void main(String[] args) {
// String 타입의 단일 요소 "Hello"로 구성된 불변 리스트 생성
List<String> singleton = Collections.singletonList("Hello");
System.out.println(singleton); // 출력: [Hello]
// UnsupportedOperationException 발생 - 변경 시도
singleton.add("World");
}
}
위 코드에서 볼 수 있듯이, Collections.singletonList() 메서드를 호출하여 단일 요소 "Hello"로 구성된 불변 리스트(singleton)를 생성합니다. 이 리스트는 [Hello]와 같이 출력됩니다.
하지만 singleton 리스트에 새로운 요소를 추가하려고 하면 UnsupportedOperationException이 발생합니다. SingletonList는 불변성을 유지하기 때문에 내부 요소를 수정할 수 없기 때문입니다.
따라서, 단일 요소로 구성된 읽기 전용 리스트가 필요한 경우나 메모리 및 리소스 효율성을 고려해야 할 때 사용할 수 있는 유용한 도구인 SingletonList를 활용할 수 있습니다.
이러한 정적 팩터리 메서드들은 코드의 가독성과 유지보수성을 향상시키고, 불필요한 객체 생성을 방지하여 성능 개선에 도움이 됩니다.
3. 생성 비용이 비싼 객체는 캐싱하여 재사용 권장
Pattern - String.matches() 보다는 Pattern인스턴스를 정적 초기화하여 캐싱해두자.
Pattern은 입력받은 정규표현식에 해당하는 유한 상태 머신(finite state machine)을 만들기 때문에 비용이 높습니다.
String.matches()
다음은 정규표현식을 활용한 가장 쉬운 방법인 String.matches()를 이용한 방법입니다.
static boolean isEmail(String email){
return email.matches("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}");
}
여기에는 String.matches()를 사용함으로써 성능에 문제를 줄 수 있습니다.
아래는 String.matches()의 코드입니다.
public final class String{
public boolean matches(String regex) {
return Pattern.matches(regex, this);
}
String.matches()에서 정규표현식용 Pattern인스턴스가 한 번 쓰고 버려져서 곧바로 가비지 컬렉션 대상이 되기 때문에
String.matches()는 정규표현식으로 문자열 형태를 확인하는 가장 쉬운 방법이지만, 반복해서 사용하기엔 좋지 않습니다.
성능 개선 - Pattern 인스턴스를 클래스 초기화(정적 초기화) 과정에서 직접 캐싱하기
public class Email{
private static fianl Pattern EMAIL_PATTERN
= Pattern.compile("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}");
static boolean isEmail(String email){
return EMAIL_PATTERN.matcher(email).matches();
}
}
책에따르면 길이가 8인 문자열을 입력했을 때 개선 전 후가 6.5배정도 빨라졌다고 합니다.
또한 Pattern인스턴스를 static final로 끄집어냄으로써 이름을 지어주어 코드 의미가 더 명확해지는 효과를 볼 수 있습니다.
해당 Pattern인스턴스를 한번도 사용하지 않을 때를 고려해서 지연 초기화를 사용하는게 좋을까?
해당 isEmail을 한번도 호출하지 않으면 Pattern 필드가 쓸데없을 수 있다.
그렇다고 지연 초기화를 하는 것은 불필요한 초기화를 없앨 수 는 있지만,
코드를 복잡하게 만들고 성능은 크게 개선되지 않을 때가 많기 때문에 권하지는 않는다고 한다.
4. Map - keySet()
Map인터페이스의 keySet()은 호출마다 새로운 Set 인터페이스가 만들어질까?
Map인터페이스를 구현하는 HashMap클래스의 keySet()을 살펴보면 아니라는 답을 얻을 수 있습니다.
transient Set<K> keySet;
public Set<K> keySet() {
Set<K> ks = keySet;
if (ks == null) {
ks = new KeySet();
keySet = ks;
}
return ks;
}
이는 불필요한 객체를 만들어내는 것을 방지하기 위함이라고 볼 수 있다.\
5. 오토박싱(auto boxing)
오토박싱은 프로그래머가 기본 타입과 박싱된 기본 타입을 섞어 쓸 때 자동으로 상호 변환해주는 기술입니다.
기본 타입과 해당 박싱 타입의 구분을 흐려주지만, 완전히 없애는 것은 아니어서 잘 못 사용하면 성능에 문제를 줄 수 있습니다.
private static long sum(){
Long sum = 0;
for (long i = 0; i <= Integer.MAX_VALUE; i++) {
sum += i;
}
return sum;
}
위의 코드는 long타입 i가 sum에 더해질 때마다 오토박싱이 되어 불필요한 Long 인스턴스가 약 231개나 만들어진다.
이는 sum을 long이 아닌 Long으로 선언했기 때문이다.
박싱된 기본 타입보다는 기본 타입을 사용하고, 의도치 않은 오토박싱이 되지 않도록 주의하자.
🐤 객체 생성을 피하고자 객체 풀(pool)을 만드는 것은 좋은 방법일까?
결론은 정말 아주 무거운 객체가 아닌이상 객체 풀을 만드는 것은 코드를 헷갈리게 하고 메모리 사용량을 늘려 성능을 떨어트릴 수 있다.
요즘 JVM의 가비지 컬렉터가 최적화가 잘 되어있기 때문에 가벼운 객체는 그냥 생성하는 것이 좋다.
다만, 데이터베이스 연결은 생성비용이 워낙 비싸 재사용하는 편이 낫다고 한다.
정리
불필요한 객체 생성은 재사용을 통해 줄일 수 있는 것이 좋다.
1. 문자열은 리터럴 값을 이용하여 생성하기
2. 정적 펙터리 메서드를 이용하면 불필요한 객체 생성을 줄일 수 있다.
3. 생성비용이 비싼 객체는 정적 초기화하는 것이 좋을 수 있다.
4. HashMap에서의 keySet()은 불필요한 객체 생성을 막기위해 똑같은 객체를 반환한다.
5. 의도치 않은 오토박싱이 되지 않도록 주의하자
'Language > JAVA' 카테고리의 다른 글
[Effective java] 상속보다는 컴포지션을 사용하라 (0) | 2023.09.09 |
---|---|
[Effective java] Comparable 구현 고려하기 (0) | 2023.09.02 |
[JAVA] Supplier<T> 인터페이스 (0) | 2023.08.22 |
[JAVA] 리터럴 String vs String 객체 (0) | 2023.08.22 |
[Effective java] 필요한 자원, 의존 객체 주입 사용하기 (0) | 2023.08.21 |