Java 정규표현식

CH.dev·2025년 8월 15일
post-thumbnail

개요

Java java.util.regex 패키지를 활용한 정규표현식(Regex) 실전 사용법 정리.
PatternMatcher를 이해하고, 실무에서 자주 쓰이는 유효성 검사, 데이터 추출 및 변환 예제를 통해 개념을 다짐. 성능 최적화와 안정성을 위한 주의사항까지 정리하는 것을 목표로 함.

💡 주요 개념

Java 정규표현식의 두 축: Pattern과 Matcher

  • Pattern: 정규표현식 패턴을 담는 '틀'. 불변(Immutable) 객체.

    • Pattern.compile(regex)을 통해 생성.
    • 컴파일 과정은 비용이 크므로, 반복 사용될 패턴은 private static final 멤버로 캐싱하여 재사용하는 것이 성능 최적화의 핵심.
    • compile 시 두 번째 인자로 Pattern.CASE_INSENSITIVE(대소문자 미구분) 같은 플래그를 전달할 수 있음.
  • Matcher: Pattern이라는 틀을 가지고 실제 문자열에 대조해보는 '검사기'. 상태를 가지는(Stateful) 객체.

    • pattern.matcher(inputString)을 통해 생성.
    • 하나의 Matcher 인스턴스는 하나의 입력 문자열에 대해서만 작업을 수행. 일회성으로 사용 후 버려짐.

핵심 메서드 비교

메서드설명사용 목적
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)매칭되는 모든 부분을 주어진 문자열로 치환한 새 문자열 반환.마스킹(개인정보), 포맷팅 등

🧠 코드 예시

1. 입력 필터링: 이메일 주소 형식 검증

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")); 
    }
}

2. 로그 파싱: 이름있는 그룹(Named Group)으로 정보 추출

로그에서 레벨, 시간, 메시지를 추출. (?<이름>...) 문법을 사용하면 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("지원하지 않는 로그 형식.");
        }
    }
}

3. 텍스트 변환: 개인정보 마스킹

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) 수량자

    • Greedy (*, +): 가능한 가장 긴 문자열을 찾으려 함. <h1>abc</h1>def<h1>ghi</h1>에서 <.+><h1>abc</h1>def<h1>ghi</h1> 전체와 매칭됨.
    • Lazy (*?, +?): 가능한 가장 짧은 문자열을 찾으려 함. 위 예시에서 <.+?><h1>abc</h1><h1>ghi</h1> 각각 매칭됨. 의도치 않은 매칭을 방지하기 위해 ?를 붙이는 습관이 중요.
  • 전후방 탐색 (Lookaround)

    • (?=...): 전방탐색(Positive Lookahead). 특정 패턴이 뒤따라오는 경우에만 매칭. \d+(?=원)은 '원' 앞의 숫자만 매칭함 ('원'은 결과에 미포함).
    • (?<=...): 후방탐색(Positive Lookbehind). 특정 패턴이 앞에 있는 경우에만 매칭. (?<=\\$)\\d+는 '뒤의숫자만매칭함(' 뒤의 숫자만 매칭함 (''는 결과에 미포함).
  • 치명적 백트래킹 (Catastrophic Backtracking)

    • (a|b)*와 같이 중첩된 수량자를 포함한 비효율적인 정규식이 특정 문자열을 만났을 때, 경우의 수가 폭발적으로 증가하며 CPU를 100% 점유하고 시스템을 멈추게 하는 현상.
    • 예방: 불필요한 중첩 그룹 피하기, 더 구체적인 패턴 사용, 독점적 수량자(*+, ++)원자적 그룹((?>...))을 사용하여 백트래킹 자체를 막는 것을 고려.
profile
더 이상 미룰 수 없다 나의 공부 나의 성장

0개의 댓글