[이펙티브 자바] 아이템6 | 불필요한 객체 생성을 피하라

제롬·2022년 1월 6일
0

이펙티브자바

목록 보기
6/25

객체를 반복해서 생성하지 말고 재사용하자.

똑같은 기능의 객체를 매번 생성하기보다는 객체 하나를 재사용하는 편이 낫다. 특히 불변 객체는 언제든지 재사용할 수 있다.

[잘못된 객체 생성]

String s = new String("bikini"); 

위 코드는 실행될 때마다 String 인스턴스를 새로 만든다. 생성자에 넘겨진 bikini 자체가 이 생성자로 만들어내려는 String 과 기능적으로 완전히 같기 때문이다. 명시적으로 new 키워드를 이용해 문자열을 생성하는것은 무의미하며 성능에 악영향만 끼친다.

[객체 재사용]

String s = "bikini";

위 코드는 매번 새로운 인스턴스를 만드는 것이 아니라 하나의 String 인스턴스를 사용한다.

불필요한 객체 생성 대신 정적 팩터리 메서드를 사용하자

생성자 대신 정적 팩터리 메서드를 제공하는 불변 클래스에서는 정적 팩터리 메서드를 사용해 불필요한 객체 생성을 피할 수 있다.

[정적 팩토리 메서드 사용 예시]

첫 번째, Boolean.valueOf(String) 사용하는 경우

Boolean boolean1 = Boolean.valueOf("true");
Boolean boolean2 = Boolean.valueOf("true");

System.out.println(boolean1 == boolean2);

두 번째, Boolean(String) 사용하는 경우

Boolean boolean3 = new Boolean("true");
Boolean boolean4 = new Boolean("true");

System.out.println(boolean3 == boolean4); 
// 주소값 비교를 위해 "==" 연산자 사용 (equals는 리터럴을 이용해 생성된 변수의 비교에 적합)

위 코드 또한 boolean(String) 생성자 대신 Boolean.valueOf(String)을 사용하는 것이 좋다. 기능적으로 같은 역할을 하는 객체를 반복적으로 생성할 필요가 없기 떄문이다. 자바 9에서 Boolean 생성자는 사용 자제(deprecated)로 되었다. 불필요한 생성자를 사용하지 않는쪽으로 발전해 나가고 있는 것이다.

객체 생성비용이 비싸다면 객체를 재사용하자.

객체 생성비용이 비싼 객체가 반복적으로 필요하다면 캐싱해서 재사용하는것이 좋다.

[정규표현식을 사용한 재사용빈도가 높고 생성비용이 비싼 경우]

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

위 코드의 문제점은 String.matches 메서드를 사용하는데 있다. 이 메서드가 내부에서 만드는 Pattern 인스턴스는, 한 번 쓰고 버려져서 곧바로 가비지 컬렉션 대상이 된다. Pattern 은 입력받은 정규표현식에 해당하는 유한상태 머신을 만들기 때문에 인스턴스 생성 비용이 높다.

[값비싼 객체를 지사용해 성능을 개선]

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})$");

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

성능을 개선하려면 위 코드처럼 필요한 정규표현식을 표현하는 (불변) Pattern 인스턴스를 클래스 초기화(정적 초기화) 과정에서 직접 생성해 캐싱해두고, 나중에 isRomanNumeral 메서드가 호출될 때마다 이 인스턴스를 재사용하게 하면된다.

[객체 재사용 성능비교]

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

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

public static void main(String[] args) {
    long start = System.currentTimeMillis();
    int max = 100000;

    for (int i = 0; i <= max; i++) {
        RomanNumber.isRomanNumeral("123123");
    }

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

    start = System.currentTimeMillis();
    for (int i = 0; i <= max; i++) {
        RomanNumber.isRomanNumeralCompile("123123");
    }

    System.out.println(System.currentTimeMillis() - start);
}
// 수행결과
반복사용 : 349
재사용 : 36

반복사용과 재사용하는 코드를 비교해보면 성능차이가 상당히 많이나는 것을 알 수 있다.

불필요한 객체 재사용은 지양하자.

어댑터(뷰)를 예로 들어보자. 어댑터는 실제 작업은 뒷단 객체에 위임하고, 자신은 제 2의 인터페이스 역할을 해주는 객체이다. 즉 뒷단 객체외에는 관리할 상태가 없으므로 뒷단 객체 하나당 하나의 어댑터만 만들어 지면 충분하다.

[같은 인스턴스를 대변하는 여러개의 인스턴스를 생성하는 경우]

final Map<String, Integer> beverage = new HashMap<>();
        
beverage.put("coke", 10);
beverage.put("cider", 9);
beverage.put("water", 8);

Set<String> keySet1 = beverage.keySet();
Set<String> keySet2 = beverage.keySet();

System.out.println("beverage Size: " + beverage.size());
System.out.println("keySet1 Size: " + keySet1.size());
System.out.println("keySet2 Size: " + keySet2.size());

keySet1.remove("water");

System.out.println("beverage Size: " + beverage.size());
System.out.println("keySet1 Size: " + keySet1.size());
System.out.println("keySet2 Size: " + keySet2.size());
// 실행결과
beverage Size: 3
keySet1 Size: 3
keySet2 Size: 3

beverage Size: 2
keySet1 Size: 2
keySet2 Size: 2

Map 인터페이스의 keySet 메서드는 Map 객체 안의 키 전부를 담은 Set뷰를 반환하는데 keySet을 호출할 때마다 매번 같은 Set 인스턴스를 반환한다.
반환된 Set인스턴스가 가변일지라도 반환된 인스턴스들은 기능적으로 모두 같다. 즉, 반환한 객체 중 하나를 수정하면 위의 코드처럼 다른 모든 객체가 따라서 바뀐다.
따라서 keySet이 뷰 객체를 여러개 만들 필요도 없고 이득도 없다.

[의도치 않은 오토박싱 객체 생성]

public static void main(String[] args) {
    long start = System.currentTimeMillis();
    Long sumAutoboxing = sumAutoboxing();

    System.out.println("sumAutoboxing: "+sumAutoboxing);
    System.out.println(System.currentTimeMillis()- start);

    start = System.currentTimeMillis();
    long sumPrimitive = sumAutoboxing();

    System.out.println("sumPrimitive: "+sumPrimitive);
    System.out.println(System.currentTimeMillis()- start);
}

private static long sumAutoboxing() {
    Long sum = 0L;
    for (long i = 0; i <= Integer.MAX_VALUE; i++) {
        sum += i;
    }

    return sum;
}

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

    return sum;
}
// 수행결과
sumAutoboxing: 2305843008139952128
6553
sumPrimitive: 2305843008139952128
5638

위의 오토박싱의 경우 sum 변수를 Long으로 선언해서 불필요한 Long 인스턴스가 만들어지기 때문에 long 타입으로 선언했을떄 보다 훨씬 느리다.

따라서, 박싱된 기본 타입보다는 기본 타입을 사용하고, 의도치 않은 오토박싱이 숨어있지 않도록 주의해야 한다.

[정리]

이번 아이템은 "객체 생성은 비싸니 피해야한다"는 이야기를 하는것이 아니다.
요즘의 JVM은 작은 객체를 생성하고 회수하는 일이 크게 부담되지 않는다. 따라서 프로그램의 명확성, 간결성, 기능을 위해서 객체를 추가로 생성하는 것은 일반적으로 좋은 일이다.

또한, 아주 무거운 객체가 아닌 이상 객체 풀을 만들지는 말자. 일반적으로 자체 객체 풀은 코드를 헷갈리게하고 메모리 사용량을 늘려 성능을 떨어뜨린다.
하지만, 데이터베이스 연결 같은 경우 생성비용이 워낙 비싸니 재사용하는편이 낫다.

정리하자면, 무조건 객체를 재사용할 것이 아니라 상황에 따라 객체 재사용 여부를 판단하여 객체를 재사용해야 한다.

[Reference]
아이템-6-불필요한-객체생성을-피하라
이펙티브-자바-아이템-6.-불필요한-객체-생성을-피하라

0개의 댓글