본문 바로가기

IT

JAVA 불필요한 객체 생성

728x90

자바에서 불필요한 객체 생성이란 무엇을 의미할까? 

똑같은 기능의 객체를 매번 생성하기보다는 객체하나를 재사용하는 편이 나을 때가 많다. 그래서 주로 말이 나오는게 모두가 아는 싱글톤패턴이지 않은가? 뭐 읽기 전용이라면 싱글톤이 유리하겠지만 멀티스레드 환경에서 해당 자원을 CUD하는 목적이면 이 싱글톤은 안티패턴이라고 한다고 한다. 공유 자원을 회손하기 때문이다. 지금 필자가 포스팅하는 내용은 싱글톤이 아닌 말그대로 불필요한 객체 생성은 주로 어떤 점에서 볼 수 있는지 기록해두려고 한다.

간단한 예제부터 우리가 잘 모르고 사용하던 응용 예제까지 살펴보도록 하자.

String s = new String("lee_seunghun");

이 문장은 실행될 때마다 String 인스턴스를 새로 만든다. 생성자로 넘겨진 "lee_seunghun" 자체가 이 생성자로 만들어내려는 String과 기능적으로 완전히 똑같다. 이 문장이 반복분이나 빈번히 호출되는 메서드 안에 있다면 이 String 인스턴스가 무수히 생성될 것이다. 

String s = "lee_seunghun"

이 문장은 새로운 인스턴스를 매번 만드는 대신 하나의 String 인스턴스를 사용한다. 나아가 이 방식을 사용한다면 같은 가상 머신안에서 이와 똑같은 문자열 리터럴을 사용하는 모든 코드가 같은 객체를 재사용함이 보장된다. 

 

뭐 이런 간단한 예제는 자바 개발자라면 어느정도 알고 있을 것이라 생각하지만 복습차원에서 설명하였다. 그러면 놓칠 수 있는 조금 더 실용적인 예시를 제시해보겠다. 

 

주어진 문자열이 유효한 로마 숫자인지를 확인하는 메서드를 작성한다고 해보자. 다음은 정규표현식을 활용한 가장 쉬운 해법이다.

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 인스턴스는 한번 쓰고 버려져서 곧바로 가비지컬렉션의 대상이 된다. Pattern은 입력받은 정규표현식에 해당하는 유한 상태 머신(finite state machine)을 만들기 때문에 인스턴스 생성 비용이 높다고 한다. 성능을 개선하려면 어떻게 해야할까? 

 

성능을 개선하려면 필요한 정규표현식을 표현하는 Pattern 인스턴스 클래스 초기화 과정에서 직접 생성해 캐싱해두고 후에 isRomanNumeral 메서드가 호출 될 때마다 이 인스턴스를 재사용하는 방식을 활용해보려 한다.

public class RomanNumerals{
    private static final Pattern ROMAN = Pattern.complie(
    	"^(?=.)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();
    }
}

이렇게 개선하면 isRomanNumeral 메서드가 호출 될 때마다 Pattern 인스턴스를 재사용하여 성능을 상당히 개선할 수 있다. 길이가 8인 문자열을 입력했을 때 개선 전에는 1.1μs 가 걸렸지만 개선 후에는 0.17μs로 6.5배 정도 빨라진 것이다. 

 

이 예제를 보니 우리가 JAVA라는 언어를 사용하면서 놓친 점이 무엇이 있을지 궁금해지지 않는가?

 

우리가 자주 사용하는 Map 인터페이스의 keySet 메서드를 생각해보자. keySet 메서드는 Map 객체 안의 키 전부를 담은 Set 뷰를 반환한다. keySet을 호출할 때마다 새로운 Set 인스턴스가 만들어질까? 아니면 매번 같은 Set 인터페이스를 반환할까?반환된 Set 인터페이스가 가변이더라도 변환된 인스턴스들은 기능적으로 모두 똑같다. 즉, 반환한 객체 중 하나를 수정하면 다른 모든 객체가 따라서 바뀐다. 모두가 똑같은 Map 인스턴스를 대변하기 때문이다. 따라서 keySet이 뷰 객체를 여러 개 만들어도 상관은 없지만 그럴 필요도 없고 이득도 없다. 

 

마지막 예로 오토박싱을 살펴 보려고 한다.오토박싱은 프로그래머가 기본 타입과 박싱된 기본 타입을 섞어 쓸 때 자동으로 상호 변환해주는 기술이다. 오토박싱은 기본 타입과 그에 대응하는 박싱된 기본 타입의 구분을 흐려주지만, 완전히 없애주는 것은 아니다.

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

sum 변수를 long이 아닌 Long으로 선언했기에 불필요한 Long 인스턴스가 약 2^31개나 만들어진다. 단순히 sum 타입을 long으로만 바꿔주면 내 컴퓨터에서는 6.3s -> 0.59s 로 빨라진다. 박싱된 기본 타입보다는 기본 타입을 사용하고 의도치 않은 오토박싱이 숨어들지 않도록 주의 해야한다.

 

JVM에서는 별다른 일을 하지 않는 작은 객체를 생성하고 회수하는 일이 크게 부담되지 않는다. 프로그램의 명확성, 간결성, 기능등을 위해 객체를 추가로 생성하는 것이라면 일반적으로 좋은 일이다. 그러니 개발자가 잘 판단하여 프로그래밍 해야한다. 방어적인 복사가 필요한 상황에서 객체를 재사용했을 때의 피해가 객체를 반복 생성하였을 때의 피해보다 훨씬 크다. 불필요한 객체 생성은 그저 코드 형태와 성능에만 영향을 주지만 방어적 복사는 실패하면 언제터져 나올지 모르는 버그와 보안 구멍으로 이어질 수 있기 때문이다.

 

 

 

 

참고) EFFECTIVE JAVA(이펙티브 자바) 3/E