본문 바로가기
Develop/Java

Java | 불필요한 객체 생성을 피하라 (Effective Java 3/E - joshua bloch)

by Hoya324 2024. 7. 4.

해당 글은 이펙티브 자바 (Effective Java 3/E - joshua bloch) 를 읽고 정리한 글입니다.

 

 

이유

1. 객채를 매번 생성하기보다는 재사용하는 편이 좋다

  • 단 하나의 인스턴스를 사용하면 객체의 재사용성이 보장된다.
  • 만약, 무거운 객체라면 매번 생성할 때마다 많은 자원이 들어갈 것이고, 인스턴스를 자주 생성하게 되면 GC가 동작하게 될 확률이 높아지고 이는 애플리케이션 성능을 저하시키는 요인 중 하나이다.

대표적인 예시 String

만약 String이 가변(mutable)이라면 같은 참조를 가지는 객체의 값을 변경할 수 있게 된다. 이는 같은 참조 & 다른 값이라는 상황을 만들 수 있기 때문에 재사용이 불가능하다.

따라서 객체 공유를 통한 재사용이 목적인 String Pool을 사용할 수 없다.

또한, 가변 상태의 객체는 같은 문자열 리터럴을 가지더라도 객체를 매번 생성해야 한다. 즉, new String(”str”)과 같이 쓸데없는 인스턴스를 만드는 경우 애플리케이션의 성능에 안 좋은 영향을 준다.

따라서, String을 불변으로 만들어 위와 같은 단점들을 제거하고 성능을 개선할 수 있게 된다.

정적 팩터리 메서드를 활용하면 불필요한 객체 생성을 피할 수 있다.

public final class Boolean implements java.io.Serializable, Comparable<Boolean> {

    public static final Boolean TRUE = new Boolean(true);
    public static final Boolean FALSE = new Boolean(false);

    // ...
    public static boolean parseBoolean(String s) {
        return "true".equalsIgnoreCase(s);
    }

    // ...
    public static Boolean valueOf(String s) {
        return parseBoolean(s) ? TRUE : FALSE;
    }
}
  • Boolean.valueOf(String)정적 팩터리 메서드를 사용하여 미리 생성된 TRUE, FALSE를 반환하는 것을 확인할 수 있다.

생성 비용이 아주 비싼 객체를 반복해서 사용하지 말아라

package effectivejava.chapter2.item6;
import java.util.regex.Pattern;

// 값비싼 객체를 재사용해 성능을 개선한다. (32쪽)
public class RomanNumerals {

    // 코드 6-2 값비싼 객체를 재사용해 성능을 개선한다.
    private static final Pattern ROMAN = Pattern.compile(
            "^(?=.)M*(C[MD]|D?C{0,3})"
                    + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");

    static boolean isRomanNumeralFast(String s) {
        return ROMAN.matcher(s).matches();
    }

    public static void main(String[] args) {
        int numSets = Integer.parseInt(args[0]);
        int numReps = Integer.parseInt(args[1]);
        boolean b = false;

        for (int i = 0; i < numSets; i++) {
            long start = System.nanoTime();
            for (int j = 0; j < numReps; j++) {
                // 성능 차이를 확인하려면 xxxSlow 메서드를 xxxFast 메서드로 바꿔 실행해보자.
                b ^= isRomanNumeralSlow("MCMLXXVI");
            }
            long end = System.nanoTime();
            System.out.println(((end - start) / (1_000. * numReps)) + " μs.");
        }

        // VM이 최적화하지 못하게 막는 코드
        if (!b)
            System.out.println();
    }


    // 코드 6-1 성능을 훨씬 더 끌어올릴 수 있다! -> 호출마다 객체 생성
    static boolean isRomanNumeralSlow(String s) {
        return s.matches("^(?=.)M*(C[MD]|D?C{0,3})"
                + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
    }
}

오토박싱을 사용할 때 주의하라

import org.junit.Test;

public class AutoBoxingTest {

    @Test
    public void autoBoxing_Test() {
        Long sum = 0L;
        for (long i = 0; i <= Integer.MAX_VALUE; i++) {
            sum += i;
        }
    }

    @Test
    public void noneBoxing_Test() {
        long sum = 0L;
        for (long i = 0; i < Integer.MAX_VALUE; i++) {
            sum += i;
        }
    }
}

String Pool-출처:https://codepumpkin.com/string-pool-java/

Long → long (auto boxing)하는 비용에 의해 불필요하게 성능이 나빠질 수 있다.

즉, 박싱된 기본 타입보다는 기본 타입을 사용하고 의도치 않은 오토박싱이 숨어들지 않도록 주의하자.

 

고려 사항

객체 생성은 비싸니 피하라는 것이 아니다

  • 프로그램의 명확성, 간결성, 기능을 위해서 객체를 추가로 생성하는 것은 좋은 일이다.

나만의 객체 풀은 삼가하자

  • 예로 String pool 이 heap 영역에 이미 있는데 굳이 새로 인스턴스를 생성해서 사용하지 않을 이유가 없다.

item 50은 “새로운 객체를 만들어야한다면 기존 객체를 재사용하지 마라”를 다룬다.

  • 방어적 복사에 대해 알 수 있다.