정규 표현식을 작성할 때 특정 패턴이 반복되는 횟수를 지정해야 하는 경우가 많습니다. 이럴 때 사용하는 것이 바로 수량자(Quantifier)입니다. 자바는 크게 세 가지 유형의 수량자를 지원하며, 각각 탐욕적(Greedy) 수량자, 게으른(Reluctant) 수량자, 그리고 소유적(Possessive) 수량자라고 부릅니다.
1. 탐욕적(Greedy) 수량자
탐욕적 수량자는 별도의 표기 없이 기본적으로 적용되는 수량자입니다. 이름처럼 '탐욕스럽게' 동작하여 입력 문자열에서 가능한 한 가장 긴 매칭(longest match)을 찾으려고 시도합니다. 만약 매칭에 실패하면 마지막 문자 하나를 제외하고 다시 매칭을 시도하는 방식(백트래킹)으로 동작합니다.
예제
import java.util.Scanner;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class Test {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.println("Enter input text: ");
String input = sc.nextLine();
String regex = "[0-9]+";
// 패턴 객체 생성
Pattern pattern = Pattern.compile(regex);
// 문자열에서 컴파일된 패턴과 매칭
Matcher matcher = pattern.matcher(input);
System.out.println("Matched text:");
while (matcher.find()) {
System.out.println(matcher.group());
}
}
}
실행 결과
Enter input text:
45545
Matched text:
45545
위 예제에서 [0-9]+는 숫자가 연속으로 등장하는 가장 긴 문자열 전체를 한 번에 매칭합니다. 이것이 바로 탐욕적 수량자의 특징입니다.
2. 게으른(Reluctant) 수량자
게으른 수량자는 비탐욕적(non-greedy) 수량자라고도 하며, 탐욕적 수량자와 정반대로 최소한만큼만 매칭합니다. 처음에는 첫 번째 문자만으로 매칭을 시도하고, 실패할 경우 입력 문자열에서 문자를 하나씩 추가하며 다시 시도합니다.
탐욕적 수량자 뒤에 물음표(?)를 붙이면 게으른 수량자로 변경됩니다. 예를 들어 +?, *?, ??와 같이 사용합니다.
예제
import java.util.Scanner;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class Test {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.println("Enter input text: ");
String input = sc.nextLine();
String regex = "[0-9]+?";
// 패턴 객체 생성
Pattern pattern = Pattern.compile(regex);
// 문자열에서 컴파일된 패턴과 매칭
Matcher matcher = pattern.matcher(input);
while (matcher.find()) {
System.out.println(matcher.group());
}
}
}
실행 결과
Enter input text:
12345678
1
2
3
4
5
6
7
8
[0-9]+?는 최소 한 개의 숫자만 매칭하려고 하므로, 입력된 숫자들이 각각 하나씩 분리되어 출력됩니다.
3. 소유적(Possessive) 수량자
소유적 수량자는 동작 방식이 탐욕적 수량자와 유사하지만 결정적인 차이가 있습니다. 소유적 수량자는 처음부터 가능한 한 많은 문자를 매칭하며, 매칭에 실패했을 때 탐욕적 수량자와 달리 백트래킹을 하지 않습니다. 즉, 한번 잡은 문자를 놓아주지 않기 때문에 '소유적'이라는 이름이 붙었습니다.
탐욕적 수량자 뒤에 플러스(+)를 붙이면 소유적 수량자가 됩니다. 대표적인 소유적 수량자로는 *+, ++, ?+, {n}+ 등이 있습니다.
예제
import java.util.Scanner;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class Test {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.println("Enter input text: ");
String input = sc.nextLine();
String regex = "[0-9]++";
// 패턴 객체 생성
Pattern pattern = Pattern.compile(regex);
// 문자열에서 컴파일된 패턴과 매칭
Matcher matcher = pattern.matcher(input);
while (matcher.find()) {
System.out.println(matcher.group());
}
}
}
실행 결과
Enter input text:
45678
45678
입력된 숫자 문자열 전체가 한 번에 매칭되어 출력됩니다. 백트래킹이 발생하지 않으므로 불필요한 재시도 없이 빠르게 처리됩니다.
마무리: 세 가지 수량자 비교 요약
| 수량자 유형 | 표기법 | 동작 방식 |
|---|---|---|
| 탐욕적(Greedy) | X+, X*, X? | 가장 길게 매칭 시도 후, 실패 시 백트래킹 |
| 게으른(Reluctant) | X+?, X*?, X?? | 가장 짧게 매칭 시도 후, 필요 시 확장 |
| 소유적(Possessive) | X++, X*+, X?+ | 가장 길게 매칭 시도, 백트래킹 없음 |
세 수량자의 차이를 이해하면 정규식의 성능을 최적화하고 의도치 않은 매칭 오류를 방지할 수 있습니다. 특히 대용량 텍스트를 처리할 때는 백트래킹을 하지 않는 소유적 수량자가 성능 면에서 유리할 수 있다는 점을 기억해 두세요.