peaches-book-study / effective-java

이펙티브 자바 3/E
0 stars 2 forks source link

Item 6. 불필요한 객체 생성을 피하라. #6

Open heon118 opened 7 months ago

heon118 commented 7 months ago

Chapter : 2. 객체 생성과 파괴

Item : 6. 불필요한 객체 생성을 피하라.

Assignee : heon118


🍑 서론

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

🍑 본론

재사용성

String s = new String("peach"); // 생성자
String s = "peach";

Boolean b = Boolean(String s); // 생성자
Boolean b = Boolean.valueOf(String s);

Java의 가상머신은 똑같은 문자열 리터럴에 대해서는 동일 코드를 사용하는 재사용성이 보장되기 때문에 생성자를 통해 String 객체를 생성하게 되면 쓸데없이 String 인스턴스를 반복해서 만들게 된다.

이 때, 정적 팩토리 메소드(static public)를 사용하면 불필요한 객체 생성을 피할 수 있다. 생성자는 매번 새로운 객체를 만들지만 팩토리 메소드는 클래스 내부에 한 번 만들어서 캐싱해놓고 사용할 수 있기 때문이다.(재사용성) 불변 객체 만이 아닌 가변 객체라 해도 사용 중 변경되지 않을 것임을 안다면 재사용 가능하다.

비싼 객체의 재사용

비싼 객체는 생성 비용이 비싸다는 것을 의미 -> 비싼 객체가 반복해서 필요하다면 캐싱해서 재사용하자

  1. 시스템의 자원을 많이 먹는 부분
    • 메모리
    • 디스크 사용량
    • 네트워크의 대역폭
  2. 데이터의 크기가 크거나 객체 내부에 여러 객체들을 포함하는 경우 or 단순 생성/소멸이 아닌 연관 관계가 복잡한 부분
    • 크기가 아주 큰 Array
    • Database Connenction
    • I/O 작업을 필요로 하는 Object
  3. 정규표현식용 Pattern(교재)
    // isRomanNumeralSlow 메서드를 호출할 때마다 비싼 객체인 정규표현식용 Pattern 생성
    // String.matches는 정규표현식으로 문자열 형태 확인하는 가장 쉬운 방법이지만 반복된 사용에선 성능 부적합
    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})$");
    }
    // 1.1µs
    
    // Pattern 인스턴스를 클래스 초기화 과정에서 직접 생성해 캐싱
    // isRomanNumeralFast 메서드를 호출될 때마다 인스턴스 재사용
    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(); } // 0.17µs


## 불필요한 객체 생성
덜 명확하거나, 심지어 직관에 반대되는 상황에서의 인스턴스 재사용하는 경우가 있다.
- 어댑터(뷰)
어댑터(또는 view)는 실제 작업은 뒷단 객체에 위임하고, 자신은 제2의 인터페이스 역할만 해주는 객체다. 즉, 뒷단 객체 외에는 관리할 상태가 없으므로 뒷단 객체 하나당 어댑터 하나씩만 만들어지면 충분하다.
Map 인터페이스의 keySet 메서드는 Map 객체 안의 키 전부를 담은 Set 뷰를 반환한다.
keySet을 호출할 때마다 같은 Set 인스턴스를 반환한다.
반환된 Set 인스턴스가 일반적으로 가변이더라도 반환된 인스턴스들은 기능적으로 모두 똑같다. 즉, 반환한 객체 중 하나를 수정하면 다른 모든 객체가 따라서 바뀐다. 모두가 똑같은 Map 인스턴스를 대변하기 때문이다.
```java
public class UsingKeySet {
    public static void main(String[] args) {
        Map<String, Integer> menu = new HashMap<>();
        menu.put("Burger", 8);
        menu.put("Pizza", 9);

        Set<String> names1 = menu.keySet();
        Set<String> names2 = menu.keySet();

        // 재사용하는 전역에서 사용하는 Map일 경우 다른 쪽에도 영향을 줄 수 있다
        names1.remove("Burger");
        System.out.println(names2.size()); // 1
        System.out.println(menu.size()); // 1
    }
}

불필요한 오토박싱을 피하려면 박싱된 기본 타입 보다는 프리미티브 타입(기본)을 사용해야 한다.

🍑 결론

객체 생성을 무조건 피하지 마라

객체생성은 비싸니 피해야 한다로 오해하면 안된다. 특히 JVM에서는 별다른 일을 하지 않는 작은 객체에 대해서는 큰 부담이 되지 않는다고 한다.

프로그램의 명확성, 간결성, 기능을 위해서는 객체를 추가로 만들 수도 있어야 한다.

또 객체 생성을 효율적으로 해보겠다고 사소한 것들도 다 캐싱하거나 자체 풀(pool)을 만들어서 유지보수하기 어려운 복잡한 프로그램을 만드는 것도 피해야 하는 부분이다.(JVM에게 위임할 부분은 위임해라 가비지 컬렉터를 신뢰하자)

[Item50]방어적 복사(defensive copy) 와 대비되는 내용이기 때문에 객체 생성을 해야하는 경우와 하지 않고 기존 것을 재사용해야 하는 부분은 개발자의 역량에 달려있다.(혹은 성능 테스트를 직접 해서 비교해보아라)

새로 만든 생성자가 필요한 경우와 재사용 가능한 불변 객체를 사용하는 구분을 할 수 있어야 한다.


Referenced by