Skills/Java

Java - 불필요한 객체 생성을 피하라

aoaa 2022. 12. 10. 23:03

 인스턴스를 사용할 때, 똑같은 기능의 객체를 매번 생성하는것 보다 객체 하나를 재사용하는 편이 나을 때가 많은데, 예시를 통해 안좋은 예를 살펴보겠습니다.


1. 예시

public class ex1 {

    public static void main(String[] args) {
        String s = new String("ROME");
        String n = new String("ROME");
    }
}

 new String(String) 으로 생성 시, 매번 새로운 주소를 할당한 인스턴스를 생성하지만 두 객체는 기능적으로 완전히 똑같습니다.

이 때, 같은 문자열을 가진 String 인스턴스를 생성한다면,

public class ex2 {

    public static void main(String[] args) {
        // s1, s2를 하나의 같은 인스턴스로 초기화
        // String 은 내부적으로 문자열 풀을 사용해서 같은 문자열이라면 같은 주소값을 가짐
        String s1 = "ROME";
        String s2 = "ROME";
    }
}

 위와 같이 하는것이 좋습니다. 위와 같은 방법에서 생성하는 String 인스턴스는 같은 문자열 리터럴을 사용한다면 같은 주소값을 사용하도록 하여, 문자열 리터럴이 같다면 같은 객체를 가지게 됩니다.


2. 정적 팩터리 메서드를 통한 불필요한 객체 생성 피하기

생성자 대신 정적 팩터리 메서드(아이템1)를 제공하는 불변클래스에서는 불필요한 객체 생성을 피할 수 있습니다.

Boolean(String) 생성자 대신 Boolean.valueOf(String) 팩터리 메서드를 사용하는 것이 더 좋습니다.

Boolean(String) 생성자는 매번 새로운 객체를 반환해주지만 Boolean.valueOf(String)은 미리 생성된 객체를 반환해줍니다.

Java Boolean의 생성자

Boolean 생성자 부분은 매번 새로운 객체를 생성하지만

 

Java Boolean.valueOf 팩터리 메서드

Boolean.valueOf (팩토리)메서드는 이미 생성된 TRUE, FALSE 객체를 리턴해줍니다.

Boolean 에서 미리 생선된 객체인 TRUE, FALSE

 Java9부터  Boolean생성자는 Deprecated되어, 사용을 자제하고 다른 방법을 사용하길 권장됩니다.

생성자는 매번 새로운 객체를 반환해야하지만(필수) 팩터리 메서드는 그렇지 않다는 점!


3. 생산 비용이 비싼경우 재사용을 고려

 생산 비용이 아주 비싼 객체도 가끔 있습니다. 여기서 '비싼 객체'반복적으로 사용되는 인스턴스로 필요하다면 캐싱하여 재사용하는것을 권장됩니다.

 이에 해당하는것이 정규표현식을 활용한 예제인데, 기본적으로 Pattern 클래스의 인스턴스를 생성하는것은 비용이 비쌉니다. 이걸 좀 빨리봤다면 프리코스에서 검증 로직을 만들 때 다른 방법을 생각해봤겠네요.)

public class ex3 {

    static boolean isRomanNumeral(String input) {
        return input.matches("^(?=.)M*(C[MD]|D?C{0,3})"
                + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
    }
}

 위 예제의 문제점은 String 클래스의 matches 메서드를 사용하는데 있습니다.

String.matches는 정규 표현식으로 문자열 형태를 확인하는 가장 쉬운 방법 이지만, 성능이 중요한 상황에서 반복해서 사용하기엔 적합하지 않습니다.

 matches 메서드 내부에서 Pattern 인스턴스가 생성되고 한번 사용한 뒤 버려져서 가비지 컬렉션 대상이 됩니다.

입력받은 정규표현식은 유한 상태머신(finite state machine)을 만들기 때문에 인스턴스 생성 비용이 비쌉니다.

 

따라서 아래와 같이 Pattern 인스턴스를 불변 정적 객체로 만들어 캐싱해두고 해당 객체를 재사용하는것이 좋습니다.

public class ex4 {

    // 캐싱 및 재사용
    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 isRomanNumeral(String input) {
        return ROMAN.matcher(input).matches();
    }
}

 

이러한 방식은 정적 Pattern 타입 필드에 어떤 정규표현식인지 이름을 붙일 수도 있습니다.


4. 불필요한 오토박싱이 일어나는 구현을 주의

오토박싱은 프로그래머가 기본 타입과 박싱된 기본 타입을 섞어 쓸 때 자동으로 상호 변환해주는 기술입니다.

오토박싱은 기본 타입과 그에 대응하는 박싱된 기본 타입의 구분을 흐려주지만, 완전히 없애주는 것은 아닙니다.

의미상으로는 다를것이 없지만 성능에는 차이가 발생합니다.

 

public class ex5 {
    public static void main(String[] args) {
        // 오토 박싱 비용으로 인해 불필요한 작업이 추가로 생김
        long start = System.currentTimeMillis();
        Long sum = 0L;

        for (long i = 0; i < Integer.MAX_VALUE; i++) {
            sum += i;
        }

        System.out.println(sum);
        System.out.println(System.currentTimeMillis() - start);
    }
}

 

위의 예제는 0부터 int타입의 최대값까지 전부 더해주는 예제입니다.

정확한 결과는 나오지만 잘못된 부분이 있습니다.

바로 sum 변수의 타입이 Long 이라서 sum += i 부분에서 계속 오토박싱이 일어나고 있습니다.

 

여기서 단순히 sum 변수의 타입을 long으로 바꾸기만해도 약 10배가량 빨라지는것을 확인할 수 있습니다.

public class ex6 {

    public static void main(String[] args) {
    	long start = currentTimeMillis();
        long sum = 0L;

        for (long i = 0; i < Integer.MAX_VALUE; i++) {
            sum += i;
        }

        System.out.println(sum);
        System.out.println(currentTimeMillis() - start);
    }
}


5. 마무리

 JVM의 성능을 고려하면 작은 객체를 생성하고 회수하는 일은 크게 부담되는 일이 아닙니다.

프로그램의 명확성, 간결성, 기능을 위해서 객체를 추가로 생성하는 것이라면 일반적으로 좋습니다. 

 

 만약 아주 무거운 객체가 아니라면 단순 객체 생성을 피하기 위해서 우리만의 객체 풀(pool)을 생성하는것은 피해야 합니다.

무거운 객체로 인해 객체 풀을 사용(대표적인 예로 데이터베이스 연결 객체)하는 것은, 일반적으로 코드를 헷갈리게 만들고 메모리 사용량을 늘려서 성능을 떨어뜨립니다. (무의미한 객체 생성을 주의하라는 의미!)