반응형

ITEM 6 "불필요한 객체 생성을 피하라"

 

 

이 ITEM 을 확인하기 전에 위 백기선님의 Effective Java 해설 영상을 보는 것을 추천한다.

열심히 문어체로 정리하고 작성하지만, 구어체를 따라 올 전달력은 없는 것 같다. 열심히 예를 들어 설명해주셔서 덕분에 이해가 잘 됐다.

 

객체를 새로 만드는 대신 하나를 재사용하는 것이 적절할 때가 있다. 불변 객체의 경우 언제든 재사용할 수 있고 가변 객체라고 하더라도 사용 중에 변경이 되지 않을 것임을 안다면 재사용할 수 있다. 재사용할 수 있음에도 불구하고 매번 같은 기능의 객체를 생성하는 것은 불필요하며 성능에 제약을 가져다 준다. 불필요한 객체를 생성하는 예를 살펴보자.

 

new String() 을 사용하지 말자.

자바의 문자열 String 을 new 로 생성하면 매번 새로운 객체를 만들게 된다.

가급적 String s = "gold-egg"; 와 같이 String 객체를 생성하는 것이 올바르다. 문자열이 같다면 리터럴 자체를 재사용한다.

 

String s1 = new String("gold-egg");
String s2 = new String("gold-egg");
System.out.println(s1 == s2); // false
System.out.println(s1.equals(s2)); // true

String s3 = "gold-egg";
String s4 = "gold-egg";
System.out.println(s3 == s4); // true
System.out.println(s3.equals(s4)); // true

 

static 팩토리 메서드를 사용하자.

Item 1 에서 살펴보았듯이 static 팩토리 메서드는 매번 새로운 객체를 만들지 않고 캐싱해 둔 객체를 리턴할 수 있다.

Boolean(String) 대신 Boolean.valueOf(String) 처럼 static 팩토리 메서드를 사용하자. 생성자는 매번 새로운 객체를 생성하기 때문에 위와 같이 불변객체라면 팩토리 메서드를 통해 캐싱된 객체를 사용하는 것이 바람직하다.

 

비싼 객체라면 재사용할 수 있는지 고려해야 한다.

객체 생성 시, 메모리나 시간이 오래 걸리는 객체를 비싼 객체라고 표현한다. 이런 비싼 객체가 반복해서 필요하다면 성능 문제가 발생할 수 있다. 캐싱하여 재사용할 수 있는지 검토해야 한다.

 

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 인스턴스가 만들어진다. matches 메서드 인자 값이 Pattern 인스턴스 재료이다. 그런데 잘 보면 매치가 일어난 다음에 scope 가 끝나버려 바로 GC 의 대상이 되는 것을 볼 수 있다.

 

Pattern 인스턴스는 정규표현식을 표현하기 위해 내부적으로 FSM 유한 상태 기계를 만드는데 이 비용이 비싸다. Pattern 인스턴스가 불변이라면 클래스 초기화 과정에 직접 생성해 캐싱해두고, 필요할 때마다 재사용한다면 성능을 향상시킬 수 있다.

 

public class RomanNumber {

    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 s) {
        return ROMAN.matcher(s).matches();
    }

}

 

개선 후 조금 더 빨라지고 코드도 더 명확해진 것을 볼 수 있다. 어떤 문자열을 매칭하는 것인지 몰랐지만 Pattern 을 바깥으로 끄집어 내면서 ROMAN 이라는 문자열을 통해 무엇을 비교하는지 명확해진 것이다. 그러나 만약 초기화 된 후 isRomanNumeral 메서드를 사용하지 않는다면 쓸데없이 객체를 생성한 꼴이 된다는 점도 알아야 한다. 처음 호출 할 때 초기화가 될 수 있도록 지연 초기화라는 방법을 생각할 수 있지만 코드는 더 복잡해지고 성능은 개선되지 않는 경우가 많아 비추천한다.

 

반대로 같은 객체라고 생각을 하지 못 하고 객체를 공유하여 사용해서 잘못된 side-effect 가 발생할 수도 있다.

 

public static void main(String[] args) {
    Map<Integer, String> apt = new HashMap<>();
    apt.put(101, "gold-egg");
    apt.put(102, "silver-egg");

    Set<Integer> keySet1 = apt.keySet();
    Set<Integer> keySet2 = apt.keySet();

    System.out.println(keySet1 == keySet2); // true

    keySet1.remove(101);
    System.out.println(keySet2.size()); // 1
    System.out.println(apt.size()); // 1
}

 

불변 객체라면 안전하게 재사용할 수 있지만 어댑터 패턴으로 생성된 뷰 객체들은 원본 객체들을 수정할 수 있기 때문에 주의가 필요하다. Map 인터페이스의 KeySet 메서드는 Map 뒤에 있는 Set 인터페이스의 뷰를 제공한다. key 들만 따로 모아놓은 Set 인터페이스인데 호출할 때마다 새로운 객체가 생성되는 것이 아니라 같은 객체를 리턴하게 된다.

 

리턴 받은 Set 객체를 변경하면 그 뒤에 있는 Map 객체도 변경하게 될 수 있다.

 

오토박싱은 불필요한 객체를 생성한다.

오토박싱과 오토 언박싱은 프리미티브 타입과 레퍼런스 타입을 섞어 사용할 때 자동으로 상호 변환해주는 기술이다.

Int <-> Integer, boolean <-> Boolean 등... 

 

public class AutoBoxingExample {

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

 

위 예제에서 sum 변수의 타입을 실수해서 Long(대문자) 로 만들었기 때문에 불필요한 Long 객체가 2의 31제곱만큼 만들어지게 된다. i 와 sum 둘 다 long 타입이었으면 별도의 객체를 만들지 않아도 되었을텐데 레퍼런스 타입 때문에 불필요한 객체가 많이 생기게 되는 것이다. 오토박싱은 프리미티브 타입과 레퍼런스 타입의 경계가 안 보여주지만 그렇다고 그 경계가 없어지진 않는다.

 

"그렇다고 객체 생성이 항상 비싸며 가급적 피해야 한다는 오해를 해서는 안 된다.

방어적인 복사를 해야 하는 경우에도 객체를 재사용하면 심각한 버그와 보안 문제가 발생할 수 있다.

불필요한 객체 생성을 피해야 한다는 점을 리마인드해야 한다."

반응형

+ Recent posts