정규 표현식 — Pattern과 Matcher로 문자열을 파싱하는 방법
로그에서 IP 주소를 추출하거나, 이메일 형식을 검증할 때 정규식을 쓰면 되는 건 아는데, 제대로 쓰고 있는 걸까요?
정규 표현식(Regular Expression)은 강력하지만, 잘못 쓰면 성능 문제나 보안 취약점까지 발생할 수 있습니다. Java의 Pattern과 Matcher를 중심으로 문법부터 성능까지 정리합니다.
기본 사용법
Pattern과 Matcher
// 1. 패턴 컴파일 (비용이 크므로 재사용 권장)
private static final Pattern EMAIL_PATTERN =
Pattern.compile("[\\w.+-]+@[\\w-]+\\.[\\w.]+");
// 2. Matcher 생성 (스레드마다 새로 만들어야 함)
Matcher matcher = EMAIL_PATTERN.matcher("contact@example.com");
// 3. 매칭 확인
if (matcher.matches()) { // 전체 문자열이 패턴과 일치하는가?
System.out.println("유효한 이메일");
}
if (matcher.find()) { // 문자열 내에서 패턴을 찾는가?
System.out.println("발견: " + matcher.group());
}
if (matcher.lookingAt()) { // 문자열 시작 부분이 패턴과 일치하는가?
System.out.println("시작 부분 일치");
}
간편 메서드
// String.matches() — 매번 Pattern을 컴파일하므로 반복 사용 시 비효율
boolean valid = "test@email.com".matches("[\\w.+-]+@[\\w-]+\\.[\\w.]+");
// String.replaceAll()
String cleaned = "Hello World".replaceAll("\\s+", " ");
// "Hello World"
// String.split()
String[] parts = "a,b,,c".split(",", -1);
// ["a", "b", "", "c"]
자주 쓰는 정규식 문법
문자 클래스
| 패턴 | 의미 |
|---|---|
. | 줄바꿈 제외 모든 문자 |
\d | 숫자 [0-9] |
\D | 숫자가 아닌 문자 |
\w | 단어 문자 [a-zA-Z0-9_] |
\W | 단어 문자가 아닌 것 |
\s | 공백 문자 |
\S | 공백이 아닌 문자 |
[abc] | a, b, c 중 하나 |
[^abc] | a, b, c가 아닌 문자 |
[a-z] | a부터 z까지 |
반복
| 패턴 | 의미 |
|---|---|
* | 0회 이상 |
+ | 1회 이상 |
? | 0 또는 1회 |
{n} | 정확히 n회 |
{n,} | n회 이상 |
{n,m} | n회 이상 m회 이하 |
앵커
| 패턴 | 의미 |
|---|---|
^ | 문자열(또는 줄) 시작 |
$ | 문자열(또는 줄) 끝 |
\b | 단어 경계 |
캡처 그룹
번호 그룹
Pattern datePattern = Pattern.compile("(\\d{4})-(\\d{2})-(\\d{2})");
Matcher m = datePattern.matcher("2026-03-19");
if (m.matches()) {
String full = m.group(0); // "2026-03-19" (전체 매칭)
String year = m.group(1); // "2026"
String month = m.group(2); // "03"
String day = m.group(3); // "19"
}
명명된 그룹
Pattern pattern = Pattern.compile(
"(?<year>\\d{4})-(?<month>\\d{2})-(?<day>\\d{2})");
Matcher m = pattern.matcher("2026-03-19");
if (m.matches()) {
String year = m.group("year"); // "2026"
String month = m.group("month"); // "03"
String day = m.group("day"); // "19"
}
비캡처 그룹
매칭은 하되 캡처하지 않으려면 (?:...)를 사용합니다.
// 캡처 그룹: group(1)에 "http" 또는 "https"가 잡힘
Pattern p1 = Pattern.compile("(https?)://(.+)");
// 비캡처 그룹: group(1)에 바로 호스트가 잡힘
Pattern p2 = Pattern.compile("(?:https?)://(.+)");
역참조
캡처한 그룹을 같은 패턴 안에서 다시 참조할 수 있습니다.
// 연속 중복 단어 찾기 (예: "the the")
Pattern duplicateWord = Pattern.compile("\\b(\\w+)\\s+\\1\\b");
Matcher m = duplicateWord.matcher("This is is a test test.");
while (m.find()) {
System.out.println("중복: " + m.group()); // "is is", "test test"
}
\\1은 첫 번째 캡처 그룹의 값을 참조합니다.
탐욕적 vs 게으른 vs 소유적 매칭
탐욕적 (Greedy) — 기본
String html = "<b>bold</b> and <i>italic</i>";
Pattern greedy = Pattern.compile("<.+>");
// 매칭: "<b>bold</b> and <i>italic</i>"
// 가능한 많이 매칭
게으른 (Lazy/Reluctant)
Pattern lazy = Pattern.compile("<.+?>");
// 매칭: "<b>", "</b>", "<i>", "</i>"
// 가능한 적게 매칭
소유적 (Possessive)
Pattern possessive = Pattern.compile("<.++>");
// 매칭 실패 — 한 번 소비한 문자를 돌려주지 않음
// 백트래킹을 하지 않으므로 성능이 좋지만 매칭이 안 될 수 있음
실전 예제
로그 파싱
private static final Pattern LOG_PATTERN = Pattern.compile(
"(?<timestamp>\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\.\\d{3})" +
"\\s+(?<level>\\w+)" +
"\\s+\\[(?<thread>[^]]+)]" +
"\\s+(?<logger>\\S+)" +
"\\s+-\\s+(?<message>.+)"
);
public record LogEntry(String timestamp, String level, String thread,
String logger, String message) {}
public static LogEntry parseLog(String line) {
Matcher m = LOG_PATTERN.matcher(line);
if (!m.matches()) return null;
return new LogEntry(
m.group("timestamp"), m.group("level"),
m.group("thread"), m.group("logger"), m.group("message")
);
}
IP 주소 추출
private static final Pattern IP_PATTERN = Pattern.compile(
"\\b(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})\\b");
public static List<String> extractIPs(String text) {
List<String> ips = new ArrayList<>();
Matcher m = IP_PATTERN.matcher(text);
while (m.find()) {
ips.add(m.group(1));
}
return ips;
}
문자열 치환
// 카멜케이스를 스네이크케이스로
private static final Pattern CAMEL_PATTERN =
Pattern.compile("([a-z])([A-Z])");
public static String toSnakeCase(String camel) {
return CAMEL_PATTERN.matcher(camel)
.replaceAll(mr -> mr.group(1) + "_" + mr.group(2).toLowerCase());
}
// "getUserName" → "get_user_name"
전방/후방 탐색 (Lookahead/Lookbehind)
매칭은 하되 결과에 포함하지 않는 패턴입니다.
// 전방 긍정 탐색: 뒤에 "원"이 오는 숫자
Pattern price = Pattern.compile("\\d+(?=원)");
// "1000원" → "1000" 매칭 (원은 결과에 미포함)
// 전방 부정 탐색: 뒤에 "원"이 오지 않는 숫자
Pattern notPrice = Pattern.compile("\\d+(?!원)");
// 후방 긍정 탐색: 앞에 "$"가 있는 숫자
Pattern dollar = Pattern.compile("(?<=\\$)\\d+");
// "$100" → "100" 매칭
// 후방 부정 탐색: 앞에 "$"가 없는 숫자
Pattern notDollar = Pattern.compile("(?<!\\$)\\d+");
성능과 ReDoS
Pattern 컴파일 캐싱
// 나쁜 예 — 매번 컴파일
public boolean validate(String input) {
return input.matches("\\d{4}-\\d{2}-\\d{2}"); // 매번 Pattern.compile 호출
}
// 좋은 예 — 컴파일 결과 재사용
private static final Pattern DATE_PATTERN =
Pattern.compile("\\d{4}-\\d{2}-\\d{2}");
public boolean validate(String input) {
return DATE_PATTERN.matcher(input).matches();
}
ReDoS (백트래킹 폭발)
// 위험한 패턴 — 중첩된 반복
Pattern dangerous = Pattern.compile("(a+)+$");
// "aaaaaaaaaaaaaaaaaX"를 매칭하면 백트래킹이 지수적으로 증가
// 위험한 패턴 — 겹치는 대안
Pattern dangerous2 = Pattern.compile("(a|a)+$");
ReDoS 방지 전략
- 중첩된 반복 피하기:
(a+)+→a+ - 소유적 수량자 사용:
(a+)+$→(a++)++$ - 원자적 그룹 사용:
(?>a+)+$ - 입력 길이 제한: 정규식 적용 전에 입력 길이를 검증
- 타임아웃 설정: Java에서는 별도 스레드로 타임아웃 구현
// 타임아웃이 있는 매칭
public static boolean matchWithTimeout(Pattern pattern, String input,
long timeoutMs) {
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Boolean> future = executor.submit(() ->
pattern.matcher(input).matches());
try {
return future.get(timeoutMs, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
future.cancel(true);
return false;
} catch (Exception e) {
return false;
} finally {
executor.shutdownNow();
}
}
유용한 플래그
Pattern.compile("hello", Pattern.CASE_INSENSITIVE); // 대소문자 무시
Pattern.compile("^line$", Pattern.MULTILINE); // ^$가 줄 단위로 매칭
Pattern.compile("hello . world", Pattern.DOTALL); // .이 줄바꿈도 매칭
Pattern.compile(
"\\d{4} # 연도\n" +
"-\\d{2} # 월\n" +
"-\\d{2} # 일",
Pattern.COMMENTS // 주석과 공백 무시
);
정리
Pattern은 불변이고 스레드 안전하므로static final로 캐싱하세요.Matcher는 상태를 가지므로 스레드마다 새로 생성해야 합니다.- 명명된 캡처 그룹(
(?<name>...))을 사용하면 코드 가독성이 크게 향상됩니다. - 탐욕적/게으른/소유적 매칭의 차이를 이해하고, 상황에 맞게 사용하세요.
- 중첩된 반복 패턴은 ReDoS 위험이 있으니, 사용자 입력에 적용할 때 특히 주의가 필요합니다.
댓글 로딩 중...