[Effective Java 3/E - 객체 생성과 파괴] Item 6-3. 불필요한 객체 생성을 피하라 - 정규표현식 처리 성능 개선
정규표현식 처리 성능 개선
Java에서 문자열 정규식 검증을 위해 흔히 String.matches()를 사용합니다. 하지만 matches()는 내부적으로 Pattern 객체를 호출마다 새로 생성합니다. 즉, 한번 쓰고 버려져서 가비지 컬렉션 대상이 되는 객체가 매번 생성되는 것 입니다. 1회성이면 차이는 없지만 루프 또는 대량 처리에서 사용하면 심각한 성능 저하를 가져올 수 있습니다.
public static void main(String[] args) {
List<String> inputs = Arrays.asList(
"1234", "abc", "9876", "12a4", "0000", "42", "99999"
);
// String.matches() - 매번 Pattern 객체 생성
for (String input : inputs) {
if (input.matches("^[0-9]+$")) {
// 로직
}
}
// Pattern 객체 재사용
Pattern pattern = Pattern.compile("^[0-9]+$"); // Pattern 객체를 변수에 저장하여 재사용
for (String input : inputs) {
if (pattern.matcher(input).matches()) {
// 로직
}
}
}
실제로 matches의 코드 구현부를 따라가보면 매번 호출 시 마다 새로운 인스턴스를 생성하여 반환함을 알 수 있습니다.
결국 아래의 코드는 for문이 실행되면서 매번 matches()를 호출하여 새로운 인스턴스를 생성 및 반환을 반복한다는 것을 알 수 있습니다. 이러한 이유로 루프, 대량 처리가 된다면 성능이 저하됩니다.
// String.matches() - 매번 Pattern 객체 생성
for (String input : inputs) {
if (input.matches("^[0-9]+$")) {
// 로직
}
}
아래의 코드는 complie()을 호출하여 Pattern 객체를 생성하고 변수에 담아 for문 안에서 재활용하는 방식입니다. 위의 코드는 매번 for문이 돌때마다 내부적으로 complie()까지 호출하여 매번 새로운 인스턴스가 생성 되었지만, 아래의 코드는 compile() 메서드를 먼저 호출하고 해당 객체를 변수에 담아 for문 안에서 재사용함으로써 성능을 끌어올릴 수 있는 것 입니다.
// Pattern 객체 재사용
Pattern pattern = Pattern.compile("^[0-9]+$");
for (String input : inputs) {
if (pattern.matcher(input).matches()) {
// 로직
}
}
아래의 코드로 두 가지 방식의 성능 차이를 확인할 수 있는데, 테스트 데이터 10만개중에서 5의 배수만(총 2만개) 숫자 문자열로 리스트에 추가하고 정규표현식을 사용하여 검사하는 로직입니다.
컴퓨터의 성능에 따라 달라질 수 있지만 저의 경우 약 4배정도 효율이 더 좋았으며, 데이터 양이 많으면 많을 수록 객체를 재사용하는 방법이 성능이 더 좋은 것을 확인할 수 있습니다.
public static void main(String[] args) {
// 테스트 데이터 생성 (10만 개)
List<String> inputs = new ArrayList<>();
for (int i = 0; i < 100_000; i++) {
inputs.add(i % 5 == 0 ? "12345" : "abc" + i);
}
// String.matches() - 매번 Pattern 객체 생성
long start1 = System.nanoTime();
long count1 = inputs.stream()
.filter(s -> s.matches("^[0-9]+$"))
.count();
long end1 = System.nanoTime();
System.out.println("[matches()] 일치 개수: " + count1 + ", 실행 시간: " + (end1 - start1) / 1_000_000.0 + "ms");
// Pattern 객체 재사용
Pattern pattern = Pattern.compile("^[0-9]+$");
long start2 = System.nanoTime();
long count2 = inputs.stream()
.filter(s -> pattern.matcher(s).matches())
.count();
long end2 = System.nanoTime();
System.out.println("[Pattern.matcher()] 일치 개수: " + count2 + ", 실행 시간: " + (end2 - start2) / 1_000_000.0 + "ms");
}