반응형

1. 생성보다는 재사용을 고려하자.

 똑같은 기능의 객체를 매번 사용하기보다는 객체 하나를 재사용하는 편이 낫다. 실제로 Boolean.valueOf() 는 호출할 때마다 객체를 생성하지 않고 내부에 캐싱된 객체를 사용한다.

Boolean.valueOf("true");

 

 우리는 문자열을 초기화할때 매번 new 생성자를 통해 생성하지 않는다. 하나의 String 인스턴스를 사용한다. 이 경우 JVM의 String pool이라는 문자열 풀에서 캐싱하게 되고, 재사용하게 된다.

String a = "abc";
String b = "abc";

System.out.println(a == b); // String Pool에서 조회 : true

String c = new String("abc");
String d = new String("abc");

System.out.println(c == d); // Heap 메모리에 새로 생성된 객체를 조회 : false

 

 


 

2. 성능을 향상시키는 재사용

 재사용을 통해 성능이 향상되는 케이스를 알아보자.


2.1. String.matcher()

static boolean isRomanNumber(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 인스턴스를 생성한다. Pattern은 인스턴스 생성 비용이 높다. 즉, 이 메서드를 호출할 때마다 비용이 비싼 인스턴스를 생성 후 한번만 사용하고 버리는 것이다.

String.matches()
Pattern.compile()

 

 이를 개선하기 위해 Pattern 인스턴스를 캐싱해두고 재사용하는 코드로 변경하였다. 책에서는 실제 속도를 비교해보니 개선 전에 비해 6.5 배의 성능 향상을 가져왔다고 한다.

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

 

 


2.2. 오토박싱 거둬내기

 오토박싱은 프리미티브 타입과 레퍼런스 타입을 섞어 쓸때 상호 변환해주는 기술이다. 코드에서 Long 타입의 sum과 long 타입의 i를 연산하는 과정에서 오토박싱이 발생하여 연산 결과를 Long 인스턴스로 생성하게 된다. Integer.MAX_VALUE가 '2의 31승 -1' 이므로, 이 만큼의 Long 인스턴스가 새로 생성되는 것이다. 실행 시간은 6초가 걸렸다.

Long sum = 0L;

long start = System.currentTimeMillis();
for(long i = 0; i <= Integer.MAX_VALUE; i++){
    sum += i;
}

System.out.println(sum);
System.out.println(System.currentTimeMillis() - start);

 

 불필요한 Long 인스턴스의 생성을 막으려면 오토박싱을 거둬내면 되며, sum의 타입을 Long에서 long으로 바꿔주면 된다. 수정 후 실행 시간 0.6초가 걸렸다. 오토박싱이 적용된 코드보다 약 10배 빨라졌다.

public static void main(String[] args) {
    long sum = 0L; // 수정

    long start = System.currentTimeMillis();
    for(long i = 0; i <= Integer.MAX_VALUE; i++){
        sum += i;
    }

    System.out.println(sum);
    System.out.println(System.currentTimeMillis() - start);
}

 


3. 혼란을 야기할 수 있는 재사용

 객체가 불변이라면 재사용해도 안전하다. 이러한 특성을 살려 자바의 기본 라이브러리에서 많이 활용하고 있는데 만약 개발자가 이런 활용 사실을 인지하지 못할 경우 오히려 혼란을 야기할 수 있다. 

 

3.1. 어댑터

 어댑터는 실제 작업은 뒷단 객체에 위임하고, 자신은 제2의 인터페이스 역할을 해주는 객체이다. 예를들어 Map 인터페이스의 keySet 메서드는 Map 안의 키를 전부 담은 어댑터를 반환한다. 다시말하면 키를 가진 객체를 새로 생성하는게 아닌 내부 객체를 재사용하여 키 정보를 반환하는 것이다.

 

 앞서 keySet 메서드는 키 정보를 내부에서 가져온다고 했지만, 필자의 경우 키를 담은 객체를 생성하여 반환하는 줄 알았다. 만약 필자와 같이 keySet의 어댑터 특성을 몰랐다면 keySet() 메서드를 활용해 두개의 키 셋을 구하고 이를 각각 활용해야 하는 상황에서 아래와 같은 결과에 대해 혼란을 느낄 수 있을 것이다.

 

Map<String, Integer> map = new HashMap<>();
map.put("도끼", 3);
map.put("활",1);

Set<String> set1 = map.keySet();
Set<String> set2 = map.keySet();

set1.remove("도끼");

System.out.println(set1.size()); // 1
System.out.println(set2.size()); // 1 (필자는 2를 예상했다 !)

 

 첨언으로 이러한 상황에서는 keySet 메서드처럼 객체의 주소를 복사하여 사용하는 방식이 아닌, 객체의 내부 값을 참조하여 복사하는 '방어적 복사' 방식을 사용해야 한다.


4. 정리

 이번 아이템은 "객체 생성은 비싸니 피해야한다"가 아니다. 요즘 JVM에서는 작은 객체를 생성하고 회수하는 일이 크게 부담되지 않기 때문이다.

 중요한 건 방어적 복사가 필요한 상황에서 객체를 재사용했을 때의 피해가 필요 없는 객체를 반복 생성했을 때의 피해보다 훨씬 크다는 사실을 인지하고 재사용을 고려해야 하는 것이다.

반응형

+ Recent posts