ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Item 6. 불필요한 객체 생성을 피하라
    백수의 개발/이펙티브 자바 2019. 7. 16. 14:58

     

    자주 사용되는 객체의 재사용

    자주 사용되는 객체가 있다면 이는 매번 생성하기보다는 객체 하나를 재사용하는것이 훨씬 빠르고 효율적이다.

    우리가 많이 사용하는 Boolean객체를 사용할 때, 아래와 같이 작성한다면 Boolean객체는 항상 새롭게 생성될 것이다.

    Boolean trueObject = new Boolean(true);
    Boolean falseObject = new Boolean(false);

    그래서 Boolean에서의 true와 false는 Boolean객체 내에서 정적 필드 변수로 가지고 있어 재활용 된다.

    따라서 아래처럼 사용하면 별도의 객체를 생성하지 않고, 기존에 만들어진 객체를 그대로 재활용 할 수 있다.

    Boolean trueObject = Boolean.TRUE;
    Boolean falseObject = Boolean.FALSE;

    게다가 Boolean객체 내 valueOf라는 정적 팩터리 메서드 또한 객체를 재활용하는 방식으로 동작한다.

    public static Boolean valueOf(boolean b) {
    	return (b ? Boolean.TRUE : Boolean.FALSE);
    }
    
    Boolean trueObject = Boolean.valueOf(true);
    Boolean falseObject = Boolean.valueOf(false);

    이는 Boolean의 TRUE와 FALSE객체가 자주 사용될 수 있어 일종의 캐싱을 해두어 객체를 재활용할 수 있도록 한것이다.

     

    이와 유사하게 String객체도 캐싱을 사용하는데 아래의 코드를 살펴보자.

    String newStringText1 = new String("text"); // 주소 : 0x0001
    String newStringText2 = new String("text"); // 주소 : 0x0002
    String newStringText3 = new String("text"); // 주소 : 0x0003
    
    String text1 = "text"; // 주소 : 0x0004
    String text2 = "text"; // 주소 : 0x0004
    String text2 = "text"; // 주소 : 0x0004

    String객체를 사용할 때 new를 통해 객체를 생성하게 되면, 계속 새로운 객체를 생성하게 된다.

    반면에 단순히 "text"와 같이 작성하게 되면, 한번 생성된 객체를 계속 재활용하여 사용하게 된다.

     

    이는 String Pool에 대한 내용인데, 나중에 자세하게 다뤄보도록 하겠다. 기본 개념은 다음과 같다.

    위 코드에서 text1을 선언하면 힙메모리에 "text"가 저장되고, 이후에 "text"를 다시 선언하면 힙메모리에 있는 "text"를 참조하는 것이다.

     

    생성 비용이 큰 객체 재사용

    생성 비용이 큰 객체가 있다면 이는 매번 생성하기보다는 객체 하나를 재사용하는것이 훨씬 빠르고 효율적이다.

    다음은 문자열이 유효한 로마 숫자인지를 확인하는 메서드를 작성한다고 해보자.

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

    이는 크게 문제가 없어 보일 뿐만 아니라 단순해 보이고 사용도 쉬워보인다. 하지만 이는 반복적으로 호출될 경우 성능상에 큰 문제를 야기할 수 있다.

     

    matches의 내부를 보면 아래와 같이 Pattern객체를 통해 regex문자열을 compile하는 것을 볼 수 있는다. 또한 compile함수가 객체를 내부적으로 생성하는 것 또한 볼 수 있다.

    public static boolean matches(String regex, CharSequence input) {
      Pattern p = Pattern.compile(regex);
      Matcher m = p.matcher(input);
      return m.matches();
    }
    
    public static Pattern compile(String regex) {
    	return new Pattern(regex, 0);
    }
    

    쉽게 말해 matches가 계속 호출되면 Pattern객체를 지속적으로 생성한다는 것이다.

     

    그래서 객체를 재사용하므로써 성능을 향상시키기 위헤서는 아래와 같이 코드를 작성할 필요가 있다.

    public class RomanNumerals{
    	private static final Pattern ROMAN = 
    		Pattern.compile("^(?=[MDCLXVI])M*D?C{0,4}L?X{0,4}V?I{0,4}$");
            
    	public static boolean isRomanNumeral(String s){
    		return ROMAN.matcher(s).matches();
    	}
    }

     

    오토박싱으로 생성된 객체

    오토박싱은 프로그래머가 기본 타입과 박싱된 기본 타입을 섞어 쓸 때 자동으로 상호 변환해주는 기술이다.

    오토박싱은 기본 타입과 그에 대응하는 박싱된 기본 타입의 구분을 흐려주지만, 완전히 없애주는 것은 아니다.

    의미상으로는 별다를 것 없지만 성능에서는 그렇지 않다. 아래 코드를 한번 보자.

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

    이 코드는 올바른 값을 내며 정상동작하기는 한다. 그러나 굉장히 느리게 동작한다.

    sum변수를 Long으로 선언하여, long타입인 i가 더해질 때 마다 long 타입이 Long객체로 오토박싱이 일어난다.

    그렇게 반복적으로 오토박싱이 일어나면서 성능이 저하된다.

     

    이 코드를 단순히 sum을 long타입으로 선언하는 것만으로도 성능이 월등히 향상되는 것을 확인할 수 있다.

     

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

     

    마무리

    이 내용들을 보고 "객체 생성은 비싸니 피해야 한다."라고 오해하지 말자. 특히 요즘 JVM에서는 별다른 일을 하지 않는 작은 객체를 생성하고 회수히는 일이 크게 부담되지 않는다.

     

    프로그램의 명확성, 간결성, 기능을 위해서 객체를 추가로 생성하는 것이라면 일반적으로 좋은 일이다.

     

    아주 무거운 객체가 아닌 다음에야 단순히 객체 생성을 피하고자 객체 풀을 만들지 말자.

    댓글 2

Designed by Tistory.