일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- mco
- 금리인상
- 오버라이딩
- 다형성
- 자바
- 미국주식
- 알고리즘
- 금리인하
- 프로그래머스
- 잉여현금흐름
- S&P500
- 접근제어자
- 백준
- 기업분석
- object
- 제태크
- 배당성장
- javascript
- 무디스
- 그리디 알고리즘
- FCF
- XLF
- 주린이
- Java
- 현금흐름표
- 인플레이션
- StringBuffer
- 객체지향
- 주식
- etf
- Today
- Total
오늘의하루
Java Pattern.matches의 오남용으로 인한 성능 문제와 최적화 방법 본문
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 객체를 한 번만 만들게 되어 성능을 개선했습니다.
'JAVA' 카테고리의 다른 글
[스레드 안전성을 위한 선택] synchronized, ReentrantLock, ReentrantReadWriteLock 비교 (0) | 2024.09.24 |
---|---|
CPU-bound와 I/O-bound 작업의 병렬 처리 (0) | 2024.05.23 |
[Test Code] Mockito를 활용한 랜덤 값 포함 함수 유닛 테스트 (0) | 2024.05.23 |
Java Stream이란? feat. 메모리 로드, 병렬 처리, 연산 흐름 (0) | 2024.05.23 |
AOP - java.lang.ClassCastException (0) | 2024.02.23 |