[Effective Java] 2장 객체 생성과 파괴 - 아이템 6. 불필요한 객체 생성을 피하라

배상규·2023년 9월 15일
0

이펙티브 자바

목록 보기
6/12
post-thumbnail

불필요한 객체를 생성하는 경우

new String() 사용하는 경우

String a = new String("test");
String b = new String("test");
String c = new String("test");

문자열 a,b,c는 결국 동일한 "test"라는 문자열을 가지군다. 하지만 실행될때 마다 String 인스턴스를 새로 만들어 버린다. 이렇게 되면 메모리를 할당하기 때문에 낭비가 발생되어 버린다.

그렇기 때문에 문자열 리터럴을 사용하여 이러한 메모리 낭비를 줄여야 한다.

String a = "test";
String b = "test";
String c = "test";

String.matches()의 사용

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 인스턴스는, 한 번 쓰고 버려지기에 가비지 컬렉션 대상이 되기 때문이다. 성능 개선을 하려면 불변 인스턴스를 클래스 초기화시 캐싱해두고 메서드 호출시 인스턴스를 재사용하면 된다.

public class RomanNumerals {

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

개선시 속도도 빨라지고 static final 필드로 이름도 정하기 때문에 코드의 의미가 잘 드러난다.

주의사항

클래스가 초기화된 후 이 메서드를 사용하지 않는 다면 ROMAN 필드는 쓸데없이 최기호가 된것이다.

어댑터는 뒷단 객체에 위임하고, 자신은 제2의 인터페이스 역할을 해주는 객체이다. 어댑터는 뒷단 객체만 관리하면 되므로 객체 하나당 어댑터 하나씩만 만들어 주면된다.

예를 들어 Map 인터페이스의 KeySet 메서드는 Map 객체 안의 키 전부를 담은 Set뷰를 반환한다. 사용자는 KeySet을 호출할 때마다 새로운 Set 인스턴스를 반환한다 생각할 수 있지만. 가변 반환된 Set 인스턴스를 반환한다.

즉 반환된 객체 중 하나를 수정하면 모든 객체가 바뀐다. 모두 똑같은 Map 인스턴스를 대변하기 때문이다. 이러한 이유로 KeySet이 뷰 객체를 여러 개 만들어도 상관 없지만, 그럴 필요도 없고 이득도 없다.

Map<String, Integer> map = new HashMap<>();
map.put("A", 1);
map.put("B", 2);

// 두 개의 KeySet 뷰를 얻습니다.
Set<String> keySet1 = map.keySet();
Set<String> keySet2 = map.keySet();

// keySet1을 수정하면 실제 맵도 수정됩니다.
keySet1.add("C");

System.out.println(map); // 출력 결과: {A=1, B=2, C=null}

이런것 처럼 Set뷰는 실제 맵의 키를 복제 하지 않고, Map 인스턴스를 대변하는 Set 뷰 객체가 생성되기에 Map 자체가 수정되어 버린다. 이는 동일한 맵 인스턴스를 참조하기 때문이다.


오토 박싱

오토박싱은 기본 타입과 박싱된 기본 타입을 섞어 쓸 때 자동으로 상호 변환해 주는 기술이다. 하지만 오토박싱은 기본 타입과 그에 대응하는 박싱된 기본 타입의 구분을 흐리게 할뿐 완전히 없애주는 것이 아니다.

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

이 코드는 정확한 답을 내기는 하나, 성능적으로는 상당히 비효율적인 코드이다.

sum 타입은 Long타입이고, i는 long 타입이다. 반복문을 돌면서 sum에 더해질때 마다 새로운 Long 인스턴스를 만들게 되며 이는 불필요한 인스턴스가 늘어난게 되며 속도가 느려지게 된다.

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


오해하지 말아야 한다.

"객체 생성은 비싸니 피해야 한다"라고 오해하면 안된다.
특히 요즘 JVM은 작은 객체를 생성하고 회수하는 일이 크게 부담되지 않는다. 그렇기 때문에 명확성, 간결성, 기능을 위해 객체를 추가로 생성하는 것이라면 좋은것 이라고 할 수 있다.
그러므로, 데이터베이스 연결과 같은 생성 비용이 비싼 경우를 제외 하면, 커스텀 객체 풀을 만들지 않아야한다.

마지막으로 방어적 복사가 필요한 상황에서 객체를 재사용 했을때의 피해가, 필요 없는 객체를 반복 생성했을 때의 피해보다 훨씬 크다는 것을 기억하자. 반복 생성의 부작용은 코드 형태와 성능에만 영향을 주지만, 방어적 복사가 실패하면 버그와 보안 문제로 직행한다.

public class ImmutableClass {
    private List<String> items;

    public ImmutableClass(List<String> items) {
        // 외부에서 전달된 리스트를 복사하여 내부에 저장합니다.
        this.items = new ArrayList<>(items);
    }

    public List<String> getItems() {
        // 내부 리스트를 그대로 반환합니다.
        return items;
    }
}

public class Client {
    public static void main(String[] args) {
        List<String> originalList = new ArrayList<>();
        originalList.add("Item 1");
        originalList.add("Item 2");

        ImmutableClass immutableObj = new ImmutableClass(originalList);

        // 클라이언트가 내부 리스트에 직접 접근하여 수정하려고 시도합니다.
        List<String> internalList = immutableObj.getItems();
        internalList.add("Item 3");

        // 원래 의도대로 불변 객체인 immutableObj를 수정하려는 시도이지만, 내부 리스트도 변경됩니다.
        System.out.println(immutableObj.getItems()); // 출력 결과: [Item 1, Item 2, Item 3]
    }
}
profile
기록에 성장을

0개의 댓글