Java Matcher 클래스 완전정복
Java Matcher 클래스
완전정복 🎯
Pattern과 Matcher를 활용한 정규식 처리의 모든 것 — 기초 개념부터 실전 예제까지 한 번에 정리합니다.
Java에서 정규식(Regular Expression)을 사용할 때 반드시 등장하는 두 클래스가 있습니다. 바로 Pattern과 Matcher입니다.
Pattern은 정규식 패턴 자체를 컴파일한 객체이고, Matcher는 그 패턴을 실제 문자열에 적용해서 매칭을 수행하는 엔진입니다. 둘 다 java.util.regex 패키지에 속합니다.
java.util.regex.Patternjava.util.regex.Matcher별도 import 없이 JDK에 기본 포함되어 있습니다.
Matcher 객체는 직접 new로 생성할 수 없습니다. 반드시 Pattern을 통해서만 얻을 수 있습니다.
"[0-9]+"
패턴 컴파일
Matcher 생성
매칭 수행
import java.util.regex.*; public class BasicMatcherExample { public static void main(String[] args) { // 1단계: 정규식을 컴파일하여 Pattern 생성 Pattern pattern = Pattern.compile("[0-9]+"); // 2단계: 대상 문자열로 Matcher 생성 Matcher matcher = pattern.matcher("주문번호 12345, 배송번호 67890"); // 3단계: find()로 순서대로 매칭 탐색 while (matcher.find()) { System.out.println("발견: " + matcher.group() + " (위치: " + matcher.start() + "~" + matcher.end() + ")"); } } }
발견: 67890 (위치: 17~22)
| 메서드 | 반환 타입 | 설명 |
|---|---|---|
| matches() | boolean |
전체 문자열이 패턴과 완전히 일치하는지 검사 |
| find() | boolean |
현재 위치에서 다음 매칭을 탐색 (반복 호출 가능) |
| find(int start) | boolean |
지정한 인덱스부터 탐색 시작 |
| lookingAt() | boolean |
문자열의 시작부분이 패턴과 일치하는지 검사 |
| group() | String |
마지막으로 매칭된 전체 문자열 반환 |
| group(int n) | String |
n번째 캡처 그룹의 매칭 문자열 반환 |
| group(String name) | String |
이름이 있는 캡처 그룹의 매칭 문자열 반환 |
| start() | int |
마지막 매칭 시작 인덱스 |
| end() | int |
마지막 매칭 종료 인덱스 (마지막 문자 다음) |
| groupCount() | int |
패턴 내 캡처 그룹의 총 개수 |
| replaceAll(String) | String |
매칭된 모든 부분을 지정 문자열로 교체 |
| replaceFirst(String) | String |
처음 매칭된 부분만 교체 |
| reset() | Matcher |
탐색 위치를 처음으로 초기화 |
| reset(CharSequence) | Matcher |
다른 입력 문자열로 교체하고 초기화 |
가장 많이 혼동되는 세 메서드의 차이를 코드로 명확히 비교해봅니다.
import java.util.regex.*; public class MatcherComparison { public static void main(String[] args) { String text = "Hello World"; Pattern p = Pattern.compile("Hello"); // matches() — 전체 문자열이 패턴과 완전 일치해야 true Matcher m1 = p.matcher(text); System.out.println("matches() : " + m1.matches()); // false // lookingAt() — 문자열의 시작이 패턴과 일치하면 true Matcher m2 = p.matcher(text); System.out.println("lookingAt() : " + m2.lookingAt()); // true // find() — 문자열 어디서든 패턴이 발견되면 true Matcher m3 = p.matcher(text); System.out.println("find() : " + m3.find()); // true // matches()를 true로 만들려면 전체를 포함하는 패턴 필요 Pattern p2 = Pattern.compile("Hello.*"); Matcher m4 = p2.matcher(text); System.out.println("matches(.*) : " + m4.matches()); // true } }
lookingAt() : true
find() : true
matches(.*) : true
정규식에서 ()로 감싼 부분이 캡처 그룹입니다. Matcher의 group(n)으로 각 그룹을 개별 추출할 수 있습니다.
import java.util.regex.*; public class GroupCapture { public static void main(String[] args) { // 날짜 패턴: (연도)-(월)-(일) String regex = "(\\d{4})-(\\d{2})-(\\d{2})"; String text = "계약일: 2024-03-15, 만료일: 2025-12-31"; Pattern p = Pattern.compile(regex); Matcher m = p.matcher(text); while (m.find()) { System.out.println("전체: " + m.group(0)); // = group() System.out.println(" 연도: " + m.group(1)); System.out.println(" 월: " + m.group(2)); System.out.println(" 일: " + m.group(3)); System.out.println("---"); } } }
연도: 2024
월: 03
일: 15
---
전체: 2025-12-31
연도: 2025
월: 12
일: 31
Java 7부터는 이름 있는 캡처 그룹을 사용할 수 있어 가독성이 훨씬 좋아집니다.
import java.util.regex.*; public class NamedGroup { public static void main(String[] args) { // (?<이름>패턴) 형태로 그룹에 이름 부여 String regex = "(?<year>\\d{4})-(?<month>\\d{2})-(?<day>\\d{2})"; Matcher m = Pattern.compile(regex).matcher("출시일: 2024-07-04"); if (m.find()) { System.out.println("연도: " + m.group("year")); System.out.println("월: " + m.group("month")); System.out.println("일: " + m.group("day")); } } }
월: 07
일: 04
(?<name>...)은 숫자 인덱스 방식보다 코드 유지보수에 훨씬 유리합니다. 그룹 순서가 바뀌어도 코드를 수정할 필요가 없습니다.
Matcher의 replace 메서드는 String.replaceAll()보다 강력합니다. 캡처 그룹의 역참조($1, $2...)를 치환 문자열에 활용할 수 있기 때문입니다.
import java.util.regex.*; public class ReplaceExample { public static void main(String[] args) { // 예제 1: 모든 숫자를 [숫자]로 감싸기 Pattern p1 = Pattern.compile("(\\d+)"); Matcher m1 = p1.matcher("항목 3개, 가격 15000원"); System.out.println(m1.replaceAll("[$1]")); // 예제 2: 날짜 형식 변환 YYYY-MM-DD → DD/MM/YYYY Pattern p2 = Pattern.compile("(\\d{4})-(\\d{2})-(\\d{2})"); Matcher m2 = p2.matcher("오늘: 2024-06-19"); System.out.println(m2.replaceAll("$3/$2/$1")); // 예제 3: 처음 발견된 이메일만 마스킹 Pattern p3 = Pattern.compile("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}"); Matcher m3 = p3.matcher("연락처: admin@example.com, help@test.com"); System.out.println(m3.replaceFirst("[이메일 숨김]")); } }
오늘: 19/06/2024
연락처: [이메일 숨김], help@test.com
예제 1: 전화번호 유효성 검사 & 포맷 통일
import java.util.regex.*; public class PhoneValidator { static final Pattern PHONE_PATTERN = Pattern.compile("(01[016789])[- ]?(\\d{3,4})[- ]?(\\d{4})"); public static boolean isValid(String phone) { return PHONE_PATTERN.matcher(phone).matches(); } public static String format(String phone) { Matcher m = PHONE_PATTERN.matcher(phone); if (m.matches()) { return m.group(1) + "-" + m.group(2) + "-" + m.group(3); } return "유효하지 않은 번호"; } public static void main(String[] args) { String[] phones = { "01012345678", "010-1234-5678", "010 1234 5678", "02-123-4567" }; for (String phone : phones) { System.out.printf("%-15s → 유효: %-5s 포맷: %s%n", phone, isValid(phone), format(phone)); } } }
010-1234-5678 → 유효: true 포맷: 010-1234-5678
010 1234 5678 → 유효: true 포맷: 010-1234-5678
02-123-4567 → 유효: false 포맷: 유효하지 않은 번호
예제 2: HTML 태그 제거
import java.util.regex.*; public class HtmlStripper { public static void main(String[] args) { String html = "<h1>제목</h1><p>내용 <b>강조</b> 텍스트</p>"; Pattern tagPattern = Pattern.compile("<[^>]+>"); String plain = tagPattern.matcher(html).replaceAll(""); System.out.println("원본: " + html); System.out.println("결과: " + plain); } }
결과: 제목내용 강조 텍스트
예제 3: 로그에서 IP 주소 추출
import java.util.*; import java.util.regex.*; public class IpExtractor { public static void main(String[] args) { String log = "[INFO] 192.168.0.1 접속\n[WARN] 10.0.0.255 차단\n[INFO] 172.16.100.5 접속"; Pattern ip = Pattern.compile( "\\b(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})\\b"); Matcher m = ip.matcher(log); List<String> ips = new ArrayList<>(); while (m.find()) { ips.add(m.group()); } System.out.println("발견된 IP: " + ips); } }
Pattern.compile()은 정규식을 파싱·최적화하는 비용이 큽니다.루프 안에서 매번 호출하면 심각한 성능 저하가 발생합니다.
import java.util.regex.*; public class PerformanceTip { // ✅ 올바른 방법: static final로 한 번만 컴파일 private static final Pattern EMAIL = Pattern.compile("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}"); public static boolean isEmail(String input) { return EMAIL.matcher(input).matches(); } // ❌ 잘못된 방법: 호출마다 컴파일 (성능 낭비) public static boolean isEmailSlow(String input) { return Pattern.compile("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}") .matcher(input).matches(); // 매번 컴파일! } }
matcher.reset(새문자열)이 더 효율적입니다.단순 포함 검사: 단순히 문자열에 특정 문자가 포함되어 있는지만 확인할 때는
String.contains()가 정규식보다 훨씬 빠릅니다. 정규식은 복잡한 패턴이 필요할 때만 사용하세요.
\\로 써야 정규식의 \가 됩니다.예) 숫자 매칭: 정규식으로는
\d+, Java 코드에서는 "\\d+"
찾고자 하는 단어가 문자열에 여러 번 등장할 때, 특정 순서의 매칭만 골라서 가져오고 싶은 경우가 있습니다.
find()는 호출할 때마다 내부 커서가 이전 매칭 끝으로 이동하므로, N번 호출하면 N번째 매칭에 도달합니다.
import java.util.regex.*; public class NthMatch { public static void main(String[] args) { String text = "사과 3개, 배 7개, 귤 12개"; Pattern p = Pattern.compile("\\d+"); Matcher m = p.matcher(text); // 방법 1: 카운터로 N번째 추출 int count = 0; while (m.find()) { count++; if (count == 3) { System.out.println("방법1 - 3번째: " + m.group()); // 12 break; } } // 방법 2: find()를 N번 연속 호출 (간결) m.reset(); // 커서 초기화 if (m.find() && m.find() && m.find()) { System.out.println("방법2 - 3번째: " + m.group()); // 12 } } }
방법2 - 3번째: 12
find() 호출 1회 → "3" 발견, 커서가 그 뒤로 이동find() 호출 2회 → "7" 발견, 커서가 그 뒤로 이동find() 호출 3회 → "12" 발견 ✅reset()을 호출하면 커서가 문자열 처음으로 돌아갑니다.
N이 유동적이라면 유틸 메서드로 뽑아두면 편합니다.
import java.util.regex.*; public class NthMatchUtil { /** * 문자열에서 패턴의 n번째(1-based) 매칭을 반환 * 없으면 null 반환 */ public static String getNthMatch(Pattern pattern, String text, int n) { Matcher m = pattern.matcher(text); int count = 0; while (m.find()) { if (++count == n) return m.group(); } return null; // n번째 매칭 없음 } public static void main(String[] args) { Pattern p = Pattern.compile("\\d+"); String text = "사과 3개, 배 7개, 귤 12개"; System.out.println(getNthMatch(p, text, 1)); // 3 System.out.println(getNthMatch(p, text, 3)); // 12 System.out.println(getNthMatch(p, text, 5)); // null } }
12
null
자동 생성된 문자열처럼 같은 키워드가 여러 번 등장하고, 그 뒤에 (코드) 형태로 값이 붙어있을 때 캡처 그룹으로 깔끔하게 추출할 수 있습니다.
예) "동업종(C18)" 에서 C18만 추출하는 경우입니다.
import java.util.regex.*; public class CodeExtractor { public static void main(String[] args) { String text = "업종분류: 동업종(A01), 이업종(B05), 동업종(C18), 동업종(D22)"; // 동업종\( : 리터럴 '동업종(' 매칭 // ([^)]+) : ')' 아닌 문자를 그룹으로 캡처 → 이게 코드값 // \) : 리터럴 ')' 매칭 Pattern p = Pattern.compile("동업종\\(([^)]+)\\)"); Matcher m = p.matcher(text); while (m.find()) { System.out.println("전체 매칭: " + m.group() // 동업종(C18) + " → 코드: " + m.group(1)); // C18 } } }
전체 매칭: 동업종(C18) → 코드: C18
전체 매칭: 동업종(D22) → 코드: D22
동업종\\(([^)]+)\\)동업종 — 고정 문자열 그대로 매칭\\( — 리터럴 ( (정규식에서 특수문자라 이스케이프 필요)([^)]+) — )가 아닌 문자 1개 이상을 캡처 그룹으로 묶음 → group(1)\\) — 리터럴 )
특정 순서의 것만 필요하다면 N번째 매칭 기법과 조합합니다.
import java.util.regex.*; public class CodeExtractorNth { public static void main(String[] args) { String text = "업종분류: 동업종(A01), 이업종(B05), 동업종(C18), 동업종(D22)"; Pattern p = Pattern.compile("동업종\\(([^)]+)\\)"); Matcher m = p.matcher(text); int target = 3; // 3번째 동업종의 코드를 원함 int count = 0; while (m.find()) { if (++count == target) { System.out.println("3번째 동업종 코드: " + m.group(1)); break; } } } }
[^)]+는 "닫는 괄호가 나오기 전까지 모든 문자"를 의미합니다.코드값이
C18처럼 짧든, FOOD_BEVERAGE_01처럼 길든 상관없이 전부 캡처합니다.괄호 안의 내용이 무엇이든 안전하게 추출할 수 있는 범용 패턴입니다.
🎯 정리
Matcher는 Pattern의 힘을 실제 문자열에 적용하는 엔진입니다.
find()로 반복 탐색, 그룹 캡처로 부분 추출, replace로 변환까지
정규식의 모든 기능을 Matcher 하나로 완성할 수 있습니다.
Pattern은 static final로 재사용 → 성능까지 잡는 Matcher 활용법을 기억하세요! ☕