ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Item 6: Avoid creating unnecessary objects
    독서/Effective Java 2021. 12. 4. 19:49

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

     

    단일 개체를 재사용하는 것은, 필요할 때마다 기능적으로 동일한 새 개체를 만드는 것보다 더 나은 경우(더 빠르거나, 자원을 더 아끼거나)가 많다. 특히, 객체가 변경 불가능한 경우(불변) 항상 재사용할 수 있다(Item 17).

     

    Ex) 매번 String Object 생성

    String s = new String("bikini");

    항상 새 String 인스턴스를 생성 => 불필요한 Object 생성 증가

    "bikini" 자체가 String 인스턴스이며, 생성자에 의해 생성된 모든 객체와 기능적으로 동일하다.

    만약 반복문에서 실행된다면? 수백만 개의 String 인스턴스가 불필요하게 생성될 수 있다.

    수정한다면?

    String s = "bikini";

     

    Object 생성 비용이 비쌀 경우 -> 캐싱해서 사용하자.

     

    생성 비용이 아주 비싼 Object인 경우, 그리고 이 Object가 반복해서 필요하다면 캐싱하여 재사용하자.

     

    해당 코드는 정규표현식으로 String s를 확인하는 메서드이다.

     

    // Performance can be greatly improved!
    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 메서드 이용에 있다.
    String.matches는 정규 표현식으로 문자열 형태를 확인하는 가장 쉬운 방법이지만, 성능이 중요한 상황에서 반복해 사용하기 적합하지 않다. 메서드 내부에서 만드는 정규표현식용 Pattern은 한번쓰고 버려져 곧바로 가비지 컬렉션 대상이 된다. 
    특히, Pattern은 생성비용이 높은 클래스 중 하나이다.

     

    만약 늘 같은 Pattern이 필요함이 보장되고 재사용 빈도가 높다면?

    아래와 같이 Pattern을 상수(static final)로 초기에 캐싱해놓고 계속 재사용하면 된다.

    import java.util.regex.Pattern;
    
    // Reusing expensive object for improved performance
    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})$");
      static boolean isRomanNumeral(String s) {
        return ROMAN.matcher(s).matches();
      }
    }

    성능 개선 포인트

    1) 필요한 정규표현식을 표현하는 (불변인) Pattern 인스턴스를 클래스 초기화(정적 초기화) 과정에서 직접 생성해 캐싱.

    2) isRomanNumeral이 호출될 때마다 Pattern 사용.

     

    생성비용이 비싼 객체라면 캐싱 방식을 고려해야 한다.
    자주 쓰는 값이라면 static final로 초기에 캐싱해놓고 재사용 하자.

     

    어댑터

    불변 객체인 경우에 안정하게 재사용하는 것이 매우 명확하다. 하지만 몇몇 경우 분명하지 않다.
    어댑터를 예로 들면, 어댑터는 인터페이스를 통해 뒤에 있는 객체로 연결해주는 view라 여러 개 만들 필요가 없다.

    같은 인스턴스를 대변하는 여러 개의 인스턴스를 생성하지 말자.

    (싱글턴 활용)

     

    HashMap의 keySet의 예를 보자.

    public class HashMap<K, V> extends AbstractMap<K, V> 
            implements Map<K, V>, Cloneable, Serializable {
        public Set<K> keySet() {
            Set<K> ks = keySet;
            if (ks == null) {
                ks = new KeySet();
                keySet = ks;
            }
            return ks;
        }
    }
    
    @Test
    void mapKeySetTest() {
      Map<String, Object> map = new HashMap<>();
      map.put("Hello", "World");
    
      Set<String> setA = map.keySet();
      Set<String> setB = map.keySet();
    
      assertThat(setA).isSameAs(setB);
    }

    Map 인터페이스의 keySet 메서드는 Map 객체 안의 키 전부를 담은 Set을 반환한다.
    하지만, 동일한 Map에서 호출하는 keySet 메서드는 같은 Map을 대변하기 때문에 반환한 객체 중 하나를 수정하면 다른 모든 객체가 따라서 바뀐다.
    따라서 keySet이 Set Object를 여러 개 만들 필요 X 이득 X.

     

    의도치 않은 오토박싱을 주의하자

    오토박싱은 기본 타입과 박싱된 기본 타입을 섞어 쓸 때 자동으로 상호 변환해주는 기술이다.
    의미상으로는 별다를 것 없지만 성능에서는 그렇지 않다(Item 61).

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

    sum변수를 long이 아닌 Long으로 사용해서 long 타입인 i가 Long 타입인 sum 인스턴스에 더해질 때마다 sum에 대응하는 i의 Long인스턴스가 약 2의 31승개나 만들어졌다.
    Long으로 선언된 변수를 long으로 바꾸면 훨씬 더 빠른 프로그램이 된다.

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

     

    오해 금지

    "객체 생성은 비싸니 피해야 한다"로 오해하면 X.

    특히나 요즘의 JVM에서는 별다른 일을 하지 않는 작은 객체생성하고 회수하는 일이 크게 부담되지 않는다.
    프로그램의 명확성, 간결성, 기능을 위해 객체를 추가로 생성하는 것이라면 일반적으로 좋은 일이다.

    그렇다고 단순히 객체 생성을 피하기 위해 자신만의 Object Pool을 만들지는 말자.
    DB Connection 같은 경우 생성 비용이 워낙 비싸니 재사용 하는 편이 낫다.
    하지만 일반적으로 자체 객체 풀은 코드를 헷갈리게 하고, 메모리 사용량을 늘리고, 성능을 떨어뜨린다.

    요즘 JVM의 GC는 상당히 잘 최적화 되어서, 가벼운 객체를 다룰 때는 직접 만든 객체 풀보다 훨씬 빠르다.

     

    재사용 vs 방어적 복사

    이번 아이템은 객체를 재사용할 것을 권장한다.

    하지만 item 50은 방어적 복사를 통해 재사용을 지양한다.
    가장 최선은 객체를 재사용할지, 방어적 복사를 통해 재사용을 안 할지 상황에 맞춰 판단하는 것이다.

     

     

    댓글

Designed by Tistory.