Java java.util.regex 패키지를 활용한 정규표현식(Regex) 실전 사용법 정리.
Pattern과 Matcher를 이해하고, 실무에서 자주 쓰이는 유효성 검사, 데이터 추출 및 변환 예제를 통해 개념을 다짐. 성능 최적화와 안정성을 위한 주의사항까지 정리하는 것을 목표로 함.
Pattern: 정규표현식 패턴을 담는 '틀'. 불변(Immutable) 객체.
Pattern.compile(regex)을 통해 생성.private static final 멤버로 캐싱하여 재사용하는 것이 성능 최적화의 핵심.compile 시 두 번째 인자로 Pattern.CASE_INSENSITIVE(대소문자 미구분) 같은 플래그를 전달할 수 있음.Matcher: Pattern이라는 틀을 가지고 실제 문자열에 대조해보는 '검사기'. 상태를 가지는(Stateful) 객체.
pattern.matcher(inputString)을 통해 생성.| 메서드 | 설명 | 사용 목적 |
|---|---|---|
matcher.matches() | 문자열 전체가 패턴과 완벽히 일치해야 true 반환. | 입력값 전체의 형식 검증 (ID, 비밀번호, 이메일 등) |
matcher.find() | 문자열 내에서 패턴과 일치하는 부분 문자열을 찾으면 true 반환. | 로그 파싱, 특정 단어/패턴 추출 등 |
matcher.lookingAt() | 문자열의 시작 부분이 패턴과 일치하면 true 반환. | 특정 접두사(prefix)로 시작하는지 검사할 때 유용. |
matcher.group(int/String) | find() 성공 후, 매칭된 내용을 가져옴. | group(0): 매치된 전체 문자열. group(1): 첫 번째 () 그룹. group("name"): 이름있는 그룹. |
matcher.replaceAll(String) | 매칭되는 모든 부분을 주어진 문자열로 치환한 새 문자열 반환. | 마스킹(개인정보), 포맷팅 등 |
matches()를 사용해 문자열 전체가 이메일 패턴에 부합하는지 확인.
참고: RFC 5322 표준을 완벽히 만족하는 정규식은 매우 복잡함. 아래 예시는 일반적인 수준의 검증임.
import java.util.regex.Pattern;
class EmailValidator {
// 일반적인 이메일 형식을 검증하는 정규표현식
private static final Pattern EMAIL_PATTERN = Pattern.compile(
"^[a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"
);
public static boolean isValid(String email) {
return email != null && EMAIL_PATTERN.matcher(email).matches();
}
public static void main(String[] args) {
System.out.printf("test.user@example.com -> %b%n", isValid("test.user@example.com"));
// 도메인의 최상위 레벨(TLD)이 없어 false
System.out.printf("user@example -> %b%n", isValid("user@example"));
}
}
로그에서 레벨, 시간, 메시지를 추출. (?<이름>...) 문법을 사용하면 group(1) 대신 group("이름")으로 값을 가져올 수 있어 가독성이 크게 향상됨.
import java.util.regex.Matcher;
import java.util.regex.Pattern;
class LogParser {
public static void main(String[] args) {
String logLine = "[ERROR] 2023-10-27 15:45:30 - Critical failure: Database connection lost.";
// 이름있는 캡처 그룹 사용: (?<level>...), (?<timestamp>...), (?<message>...)
String regex = "^\\[(?<level>INFO|WARN|ERROR|DEBUG)\\]\\s" +
"(?<timestamp>\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2})\\s-\\s" +
"(?<message>.*)$";
Pattern logPattern = Pattern.compile(regex);
Matcher matcher = logPattern.matcher(logLine);
if (matcher.find()) {
System.out.println("로그 파싱 성공!");
System.out.println(" - 로그 레벨: " + matcher.group("level"));
System.out.println(" - 시간: " + matcher.group("timestamp"));
System.out.println(" - 메시지: " + matcher.group("message").trim());
} else {
System.out.println("지원하지 않는 로그 형식.");
}
}
}
replaceAll()을 활용하여 텍스트 내의 전화번호를 찾아 중간 자리를 *로 마스킹.
import java.util.regex.Pattern;
class DataMasker {
// 01x-xxxx-xxxx 또는 01x-xxx-xxxx 형식의 전화번호 패턴
// 그룹 1: 앞자리, 그룹 2: 중간자리, 그룹 3: 끝자리
private static final Pattern PHONE_PATTERN =
Pattern.compile("(01[016789])-(\\d{3,4})-(\\d{4})");
public static String maskPhoneNumber(String text) {
if (text == null) return "";
// $1, $3은 첫번째, 세번째 그룹을 의미. 중간자리는 ****로 치환.
return PHONE_PATTERN.matcher(text).replaceAll("$1-****-$3");
}
public static void main(String[] args) {
String originalText = "문의는 010-1234-5678 또는 011-333-9999로 연락주세요.";
String maskedText = maskPhoneNumber(originalText);
System.out.println("원본: " + originalText);
System.out.println("마스킹: " + maskedText);
// 출력: 마스킹: 문의는 010-****-5678 또는 011-****-9999로 연락주세요.
}
}
탐욕적(Greedy) vs 게으른(Lazy) 수량자
*, +): 가능한 가장 긴 문자열을 찾으려 함. <h1>abc</h1>def<h1>ghi</h1>에서 <.+>는 <h1>abc</h1>def<h1>ghi</h1> 전체와 매칭됨.*?, +?): 가능한 가장 짧은 문자열을 찾으려 함. 위 예시에서 <.+?>는 <h1>abc</h1>와 <h1>ghi</h1> 각각 매칭됨. 의도치 않은 매칭을 방지하기 위해 ?를 붙이는 습관이 중요.전후방 탐색 (Lookaround)
(?=...): 전방탐색(Positive Lookahead). 특정 패턴이 뒤따라오는 경우에만 매칭. \d+(?=원)은 '원' 앞의 숫자만 매칭함 ('원'은 결과에 미포함).(?<=...): 후방탐색(Positive Lookbehind). 특정 패턴이 앞에 있는 경우에만 매칭. (?<=\\$)\\d+는 ''는 결과에 미포함).치명적 백트래킹 (Catastrophic Backtracking)
(a|b)*와 같이 중첩된 수량자를 포함한 비효율적인 정규식이 특정 문자열을 만났을 때, 경우의 수가 폭발적으로 증가하며 CPU를 100% 점유하고 시스템을 멈추게 하는 현상.*+, ++)나 원자적 그룹((?>...))을 사용하여 백트래킹 자체를 막는 것을 고려.