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 활용법을 기억하세요! ☕