똑같은 기능의 객체를 매번 생성하기보다, 객체 하나를 재사용하는게 나을 때가 있다.
특히, 불변 객체는 언제든지 재사용할 수 있고, 생성이 비싼 객체들을 따로 객체 pool 관리하며 재사용 할 수 있다.
String s = new String ("pearl" ); // 계속 스트링 객체 생성, 사용 x
String s = "pearl" ; // 하나의 스트링 인스턴스 사용
이전에 Item 01 에서 정적 팩토리를 통해 객체를 새로 생성하지 않고 이미 생성된 객체를 반환해주는 것을 보았다.
정적 팩토리를 활용해 매번 객체를 생성하지 않게 만들 수 있음을 기억하자.
비싼 객체가 반복해서 필요할 때, 캐싱해서 사용하는 것이 좋다.
자신이 생성한 객체가 비싼 객체인지, 항상 주의해야 한다.
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 인스턴스는 한 번 쓰고 버려져 GC 대상이 된다.
Pattern은 입력받은 정규 표현식에 해당하는 유한 상태 머신 을 만들기 때문에 인스턴스 생성 비용이 높다.
그러나 이 메서드는 로직 상 여러 번 호출될 수 있기 때문에, Pattern 인스턴스를 직접 생성해 캐싱해두고, 메서드가 호출될 때 마다 캐싱된 인스턴스를 재사용하는 것이 좋다.
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 ();
}
}
이렇게 개선하면 isRomanNumeral 메서드가 빈번히 호출되는 상황에서 성능을 상당히 높일 수 있다.
어댑터
Map 인터페이스의 keySet 메서드
프로그래머가 원시 타입과 박싱된 기본 타입을 섞어서 쓸 때, 자동으로 변환해주는 기술이다.
Auto Boxing 은 원시 타입과 박싱된 타입의 구분을 흐리게 만들지만, 완전히 없애주지는 않는다.
특히, 자동으로 wrapping 해주다가 성능 문제를 만드는 경우가 있다.
private static long sum () {
Long sum = 0L ;
for (long i =0 ; i <= Integer .MAX_VALUE ; i ++){
sum += i ;
}
return sum ;
}
여기서 sum 변수를 원시 타입인 long 이 아니라 wrapping 된 Long으로 사용하고 있다.
이 문제 때문에 불필요한 Long 인스턴스가 반복문의 범위인 Integer.MAX_VALUE 만큼 생성된다.
따라서, boxing 되어있는 기본 타입보다, 원시 타입을 사용하고, 코드 중에 의도치 않은 auto boxing 이 숨어들지 않도록 해야한다.
+ 주의: 객체 생성을 무조건 피해야하는 것은 아니다.
가벼운 객체라면 몇 번 새로 생성해서 쓰는게 오히려 GC에 부담되지 않는다. (비용이 크지 않음)
객체 재사용은 무거운 객체, 생성 비용이 비싼 객체를 대상으로 고려하는게 일반적이다.
또한, 방어적 복사가 필요한 상황에서, 객체를 복사하려는 생성을 피해선 안된다.
기존 객체를 변경해서 사용하는 것보다, 방어적 복사를 통해 복사한 객체를 변경해야하는 경우 (기존 객체의 동작을 해칠 위험이 있거나, 안전성 보장이 필요할 때)
이런 방어적 복사의 개념은 Kotlin 에서 더 명확하게 언어적 차원으로 다룬다. (data class 의 copy() 메서드)
+ 추가: 유한 상태 머신과 Pattern.compile() 이 하는 일
유한 상태 머신이 무엇인지?
FSM은 "입력의 흐름에 따라 정해진 상태를 바꾸며 동작하는 시스템"을 추상화한 모델
정규식을 처리하기 위한 내부 구조에서, 입력 문자열을 한 글자씩 읽으면서 현재 상태에서 다음 상태로 전이하는 방식으로 일치 여부를 판단한다. (FSM 개념)
복잡한 정규식일수록 더 많은 상태와 전이 조건이 필요해져 구성 비용이 비싸진다.
Pattern.compile() 을 하면 무슨 일이 생기는지?
Pattern.compile() 은 정규식을 FSM으로 바꾸어 효율적인 매칭을 가능하게 한다.
문자열로 된 정규식을 분석(파싱)하여 FSM으로 컴파일한다.
이 과정에는 정규식 파싱, 트리 생성, 상태 머신 구성 등 복잡한 작업이 포함되기 때문에 캐싱해서 재사용하는게 효율적이다.