티스토리 뷰

객체를 재사용해야 할 때

 똑같은 기능의 객체를 매번 생성하기보다는 객체 하나를 재사용하는 편이 나을 때가 많다.

String s = new String("example");

 위 문장은 실행될 때마다 String 인스턴스를 새로 만든다. "example" 이라는 문자열의 쓰임새와 목적이 완전히 동일함에도 불구하고, 불필요한 인스턴스를 계속 생성한다.

Stirng s = "example";

 위 코드는 매번 새로운 인스턴스를 생성하는 대신 하나의 String 인스턴스를 사용한다. JVM은 동일한 문자열 리터럴에 대해 인스턴스를 공유하도록 설계되어 있다.


정적 팩토리 메서드를 활용해 객체생성 효율을 높여보자

 생성자 대신 정적 팩토리 메서드를 제공하는 불변 클래스에서는 이를 활용해 불필요한 객체 생성을 피할 수 있다.

new Boolean("true");
Boolean.valueOf("true");

 Boolean 클래스의 인스턴스 생성에서는 valueOf 메서드를 사용하는 것이 좋다. 생성자는 호출할 때마다 매번 새로운 객체를 만들지만, 팩토리 메서드는 그렇지 않다.


생성 비용이 아주 비싼 객체는 캐싱하여 재사용하자

 생성 비용이 아주 비싼 객체가 반복해서 필요하다면 캐싱하여 재사용하는 것이 좋다. 대표적인 예시로는 정규표현식의 사용을 들 수 있겠다.

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 클래스는 입력받은 정규 표현식에 해당하는 유한 상태 머신(finite state machine)을 만들기 때문에 인스턴스 생성비용이 높다.

 

 위와 같은 코드의 성능 개선은 다음과 같이 Pattern 인스턴스를 글래스 초기화 과정에서 직접 생성해 캐싱해두고, 나중에 isRomanNumeral 메서드 호출시에 이 인스턴스를 재사용하는 방식으로 변경하면 된다.

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

 위 코드의 ROMAN 필드가 실제로 사용되는 시점에 초기화를 진행하는 지연초기화 기능을 구현하는 경우도 있다. 그런데 이러한 설계는 별로 권하지 않는다고 한다. 지연초기화는 코드를 복잡하게 만드는 것에 비해 성능개선의 폭이 작기 때문이다.


불필요한 객체를 만들어내는 오토박싱 (Auto Boxing)

 오토박싱은 프로그래머가 기본 타입과 그에 대응하는 박싱된 기본 타입(Wrapper Class 라고도 한다)을 섞어 쓸 때, 이를 자동으로 상호 변환해 주는 기술이다. 다음 코드는 오토박싱에 의해 불필요한 성능낭비가 발생한 좋은 예시이다.

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

 위 코드의 문제는 sum 변수의 선언에 있다. 타입을 long 이 아닌 Long 클래스로 선언해서 불필요한 클래스 인스턴스가 2^31 개나 만들어진다. 당연히 아주 큰 성능의 낭비가 발생할 것이다. 박싱된 기본타입보다는 기본타입을 사용하고, 의도치 않은 오토박싱이 숨어들지 않도록 주의해야 한다.


정리

 이번 아이템은 "객체 생성은 비싸니 피해야 한다" 라는 주제는 아니다. 요즘의 JVM에서는 별다른 일을 하지 않는 작은 객체를 생성하고 회수하는 일은 그리 부담되지 않는다. 프로그램의 명확성, 간결성, 기능을 위하는 일이라면 작은 성능의 희생정도는 감수하는게 더 나을 수도 있다.

 

 아주 무거운 객체가 아닐 때에는 굳이 객체 풀(pool)을 관리할 필요도 없다. 일반적으로 자체 객체 풀은 코드를 햇갈리게 만들고, 메모리 사용량을 올리며, 성능을 떨어뜨린다. 요즘의 JVM은 최적화 수준이 높아져서 일반적으로 구현하는 커스텀 객체 풀보다 훨씬 빠르다. 물론 데이터베이스 연결과 같이 생성비용이 워낙 비싸 재사용하는 편이 나은 경우도 있다.

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday