-
Java Pattern.matches의 오남용으로 인한 성능 문제와 최적화 방법Development/JAVA 2024. 12. 23. 12:14728x90반응형
Java에서는 Pattern.matches 메소드를 통해 정규표현식을 쉽게 사용할 수 있지만 내부 동작 원리를 제대로 이해하지 못하면 성능 문제가 발생할 수 있습니다.
Pattern.matches의 동작 원리
Pattern.matches(String regex, CharSequence input)는 아래와 같은 과정을 거쳐 입력 문자열을 매칭합니다.
> 정규표현식 컴파일
메소드 호출 시 입력받은 정규표현식(regex)을 NFA(비결정적 유한 오토마타, Non-deterministic Finite Automaton)에서 정규표현식에 따라 DFA(결정적 유한 오토마타, Deterministic Finite Automaton)로 변환하며 이 과정에서 정규표현식을 해석하고 효율적으로 실행될 수 있는 내부 구조를 생성하는데 상당한 비용이 소모됩니다.
> 매칭 실행
컴파일된 NFA or DFA를 사용하여 입력 문자열(input)을 매칭합니다.
Pattern.matches 내부 코드
public static boolean matches(String regex, CharSequence input) { Pattern p = Pattern.compile(regex); Matcher m = p.matcher(input); return m.matches(); }Pattern p = Pattern.compile(regex);
- 정규표현식(regex)을 NFA로 변환하고 복잡한 표현식의 경우 NFA 후에 다시 DFA로 변환합니다.
- DFA는 매칭 속도가 NFA보다 빠르며 메모리 소모가 큽니다.
Matcher m = p.matcher(input);
- 입력 문자열과 매칭을 위한 준비 단계로, Pattern 객체를 기반(NFA or DFA)으로 상태를 초기화합니다.
m.matches();
- 입력 문자열과 정규표현식을 매칭하는 부분입니다.
결론
Pattern.matches 메소드를 매번 호출하면 다음과 같은 성능 문제가 발생할 수 있습니다.
- 비싼 compile 과정
- 매번 정규표현식을 NFA or DFA로 변환하는 과정이 반복되어 자원 낭비가 발생합니다.
- 재사용 불가능
- 동일한 정규표현식을 가지고 매번 새롭게 Pattern 객체를 생성하므로 메모리와 자원 낭비가 발생합니다.
성능 테스트 코드
아래 테스트 코드를 사용하여 실제 성능 차이를 확인할 수 있습니다.
@BeforeEach public void beforeEach() { inputs = createStrings(1000); } @Test public void prepareToCompile() { Pattern pattern = Pattern.compile(regex); long startTime = System.nanoTime(); for (String input : inputs) { pattern.matcher(input).matches(); } long endTime = System.nanoTime(); long runTime = endTime - startTime; System.out.println("prepareToCompile 시간: " + runTime / 1_000_000 + " ms"); } @Test public void notPrepareToCompile() { long startTime = System.nanoTime(); for (String input : inputs) { Pattern pattern = Pattern.compile(regex); // 매번 Pattern.compile 호출 pattern.matcher(input).matches(); } long endTime = System.nanoTime(); long runTime = endTime - startTime; System.out.println("notPrepareToCompile 시간: " + runTime / 1_000_000 + " ms"); }- prepareToCompile 시간: 79ms
- notPrepareToCompile 시간: 200ms
테스트 결과 : Pattern.compile을 매번 호출하는 것과 한 번만 호출하는 것의 성능 차이가 있음을 알 수 있습니다.
적용 사례 (예시)
레거시 프로젝트를 유지보수하는 과정에서 아래와 같은 코드가 성능 문제를 일으킬 수 있음을 발견했습니다.
public class Example { private static final String regex = ".*[a-zA-Z].*\d.*[^a-zA-Z\d\s].*"; public boolean isValidExample(List<String> inputs) { if (inputs.isEmpty()) { throw new IllegalArgumentException("Input list cannot be empty."); } for (String input : inputs) { if (!Pattern.matches(regex, input)) { return false; } } return true; } }이 코드에서는 Pattern.matches를 매번 호출하고 있기 때문에, inputs의 길이에 따라 정규표현식이 반복해서 컴파일되기 때문에 이를 최적화하려면 Pattern 객체를 미리 생성하여 재사용하는 방식으로 변경해야 합니다.
public class Example { private static final String regex = ".*[a-zA-Z].*\d.*[^a-zA-Z\d\s].*"; private static final Pattern pattern = Pattern.compile(regex); public boolean isValidExample(List<String> inputs) { if (inputs.isEmpty()) { throw new IllegalArgumentException("Input list cannot be empty."); } for (String input : inputs) { if (!pattern.matcher(input).matches()) { return false; } } return true; } }이렇게 변경하면 Pattern 객체를 한 번만 만들게 되어 성능을 개선했습니다.
728x90반응형'Development > JAVA' 카테고리의 다른 글
[레거시 코드와 데이트] SimpleDateFormat의 함정 (0) 2025.02.21 JAVA의 컨텍스트 스위칭이 비싼 이유? (1) 2025.01.10 [스레드 안전성을 위한 선택] synchronized, ReentrantLock, ReentrantReadWriteLock 비교 (1) 2024.09.24 CPU-bound와 I/O-bound 작업의 병렬 처리 (0) 2024.05.23 [Test Code] Mockito를 활용한 랜덤 값 포함 함수 유닛 테스트 (0) 2024.05.23