ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Java Pattern.matches의 오남용으로 인한 성능 문제와 최적화 방법
    Development/JAVA 2024. 12. 23. 12:14
    728x90
    반응형

    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
    반응형
Designed by Tistory.