주소나 연락처가 섞인 텍스트에서 원하는 부분만 뽑아야 할 때 정규식만 한 게 없습니다. 엑셀에서 붙여 넣은 고객 목록, 크롤링한 업체 정보, 사람이 손으로 입력한 주소 칸 같은 데이터는 형식이 제각각이라 split이나 indexOf로 버티다 보면 예외 처리만 늘어납니다. 메타문자 의미부터 짚고, 이어서 전화번호와 한국 주소를 찾는 패턴을 상수로 모아 둔 클래스를 보겠습니다.

메타문자, 정확한 뜻부터
인터넷에 돌아다니는 정리 중에는 *와 +를 헷갈리게 써 놓은 것이 꽤 있어서, 직접 테스트해 보고 맞는 의미로 적었습니다.
| 기호 | 의미 | 예 |
|---|---|---|
. |
아무 문자 하나 (줄바꿈 제외) | ab. → abc, abd |
? |
바로 앞 요소가 0개 또는 1개 | ab?c → ac, abc |
* |
바로 앞 요소가 0개 이상 | ab* → a, ab, abbb |
+ |
바로 앞 요소가 1개 이상 | ab+ → ab, abbbb |
^ |
문자열(또는 줄)의 시작 | ^a → a로 시작 |
$ |
문자열(또는 줄)의 끝 | ab$ → ab로 끝남 |
[ ] |
괄호 안 문자 중 하나 | [ab] → a 또는 b |
[^ ] |
괄호 안 문자를 뺀 나머지 중 하나 | [^a]bc → bbc, xbc (abc는 제외) |
( ) |
묶음(그룹) | (ab)+ → ab, abab, ababab |
{n} |
바로 앞 요소를 n번 반복 | ab{2} → abb, (ab){2} → abab |
\| |
둘 중 하나(OR) | a\|b\|c → a, b, c 중 하나 |
여기서 많이 틀리는 게 수량자의 범위입니다. ab{2}의 {2}는 바로 앞 글자 b에만 걸리기 때문에 abb가 됩니다. ab 전체를 두 번 반복하려면 (ab){2}처럼 괄호로 묶어야 합니다. *도 "앞 문자가 없어도 된다"는 점이 +와의 차이입니다.
POSIX 문자 클래스도 함께 자주 씁니다. \p{Digit}은 숫자, \p{Space}는 공백류(스페이스, 탭, 줄바꿈 등), \p{Punct}는 특수기호를 뜻합니다. 이 자체로 무언가를 지우는 건 아니고, str.replaceAll("\\p{Space}", "")처럼 치환 메서드와 함께 써야 "공백 제거"가 됩니다. 자바 문자열 안에서는 역슬래시를 두 번 써야 한다는 점도 잊기 쉽습니다.
패턴을 상수 클래스로 모아 두는 방식
아래 클래스는 전화번호, 시·도, 시·군·구, 읍·면·동, 도로명, 번지, 산번지, 회사명 앞뒤의 (주)·(사)·(재) 같은 패턴을 static final 문자열로 정의해 두고, 몇 개를 이어 붙여 큰 패턴을 만드는 메서드를 제공합니다.
/**
* 패턴 정의
* @author lsh
*
*/
public class PatternDefine {
/**
* 전화번호 검색 패턴 02-472-2012 024722012
*/
public static final String PATTERN_PHONE = "(\\(?\\d{2,3}\\)?-?\\d{3,4}-?\\d{4})|(\\d{9,11})";
/**
* 읍,면,동 구분
*/
public static final String PATTERN_ADDRESS_UM_MYUN_DONG = "(\uC74D)|(\uBA74)|(\uB3D9)";
/**
* 시,군,구 구분
*/
public static final String PATTERN_ADDRESS_SI_GUN_GU="(\uC2DC)|(\uAD70)|(\uAD6C)";
/**
* 통합 주소 검색 읍12,면12,동12,가12
*/
public static final String PATTERN_ADDRESS_UM_MYUN_DONG_GA_BUNJI = "(\uC0B0\\d{1,5})|(\uBA74\\d{1,5})|(\uB3D9\\d{1,5})|(\uAC00\\d{1,5})|(\uB9AC\\d{1,5})";
/**
* 공백 제거 패턴
*/
public static final String PATTERN_BLANK = "\\p{Space}";
/**
* 한글 구분
*/
public static final String PATTERN_HANGUL = "[\\x{ac00}-\\x{d7af}]";
/**
* 기호중에서 - or ~ 둘중에 하나가 있는경우 구분
*/
public static final String PATTERN_HYPHEN_OR_SWUNG_DASH ="(~|-)";
/**
* 시,도 - 시,군,구 구분
*/
public static final String PATTERN_ADDRESS_SI_DO_SI_GUN_GU = "(" + PATTERN_HANGUL + "+(\uC2DC|\uB3C4)+" + PATTERN_HANGUL + "+(\uC2DC|\uAD70|\uAD6C))";
/**
* 도로명 주소 뒤에 붙는 글자
* ex
* 1. 부산광역시 중구 중구로 지하31 (신창동4가) -> 지하
*/
public static final String PATTERN_ROAD_SUFFIX_TEXT = "(\uC9C0\uD558)";
/**
* 도로명 주소 뒤에 붙는 패턴 ex)
* <br> 앞에 접두사 로|길
* <br> 1.숫자
* <br> 2.숫자+번지
* <br> 3.숫자(~|-)번지
* <br> 4.숫자(~|-)번지
* <br> 한글로 시작하는경우
* <br> 1.한글+숫자
* <br> 2.한글+숫자+번지
* <br> 3.한글+숫자(~|-)번지
* <br> 4.한글+숫자(~|-)번지
*/
public static final String PATTERN_ROAD_SUFFIX =
"(" +
"(" +
"(\\d{1,5})"+
"|(\\d{1,5}\uBC88\uC9C0)"+
"|(\\d{1,5}(~|-)\\d{1,5})" +
"|(\\d{1,5}(~|-)\\d{1,5}\uBC88\uC9C0)"+
")"+
"|"+
"("+
"("+PATTERN_ROAD_SUFFIX_TEXT+"\\d{1,5})"+
"|("+PATTERN_ROAD_SUFFIX_TEXT+"\\d{1,5}\uBC88\uC9C0)"+
"|("+PATTERN_ROAD_SUFFIX_TEXT+"\\d{1,5}(~|-)\\d{1,5})" +
"|("+PATTERN_ROAD_SUFFIX_TEXT+"\\d{1,5}(~|-)\\d{1,5}\uBC88\uC9C0)"+
")"+
")";
/**
* 도로명 주소 패턴 ex) (한글|(숫자(~|-)숫자)|숫자)로|길
*/
public static final String PATTERN_ROAD = "(([\\x{ac00}-\\x{d7af}]|(\\d{1,5}(~|-)\\d{1,5})|\\d{1,5})+(\uB85C|\uAE38))";
/**
* 한글 + 읍,면,동,가,리
*/
public static final String PATTERN_ADDRESS_UM_MYUN_DONG_GA_RI = "([\\x{ac00}-\\x{d7af}]+((\uC74D)|(\uBA74)|(\uB3D9)|(\uAC00)|(\uB9AC)))";
/**
* 한글 + 읍,면,동,가
*/
public static final String PATTERN_ADDRESS_UM_MYUN_DONG_GA = "([\\x{ac00}-\\x{d7af}]+((\uC74D)|(\uBA74)|(\uB3D9)|(\uAC00)))";
/**
* 한글 + 리
*/
public static final String PATTERN_ADDRESS_RI = "([\\x{ac00}-\\x{d7af}]+(\uB9AC))";
/**
* 번지 112-11, 11
*/
public static final String PATTERN_ADDRESS_BUNJI ="(\\d{1,5}(~|-)\\d{1,5}|\\d{1,5})";
/**
* 산번지 112-112
*/
public static final String PATTERN_ADDRESS_SAN_BUNJI ="(\uC0B0(\\d{1,5}(~|-)\\d{1,5}|\\d{1,5}))";
/**
* 회사이름 앞,뒤 글자
*/
public static final String PATTERN_COMPANY_PREFIX_AND_SUFFIX = "(\\(\uC8FC)\\)|(\\(\uC0AC)\\)|(\\(\uC7AC)\\)";
/**
* 특수문자
*/
public static final String PATTERN_SPECIAL_LETTERS = "\\p{Punct}";
/**
* 시,도 & 시,군,구 & 도로명 주소 분류 패턴
* @return
*/
public static String toSidoSigunguRoad() {
StringBuffer buffer = new StringBuffer();
buffer = new StringBuffer();
buffer.append(PATTERN_ADDRESS_SI_DO_SI_GUN_GU);
buffer.append(PATTERN_ROAD);
buffer.append(PATTERN_ROAD_SUFFIX);
return buffer.toString();
}
/**
* 시,군,구 & 도로명 주소 분류 패턴
* @return
*/
public static String toSigunguRoad() {
StringBuffer buffer = new StringBuffer();
buffer.append(PATTERN_ADDRESS_SI_GUN_GU);
buffer.append(PATTERN_ROAD);
buffer.append(PATTERN_ROAD_SUFFIX);
return buffer.toString();
}
/**
* 도로명 주소 분류 패턴
* @return
*/
public static String toRoad(){
StringBuffer buffer = new StringBuffer();
buffer.append(PATTERN_ROAD);
buffer.append(PATTERN_ROAD_SUFFIX);
return buffer.toString();
}
/**
* 통합 주소 검색 패턴
* @return
*/
public static final String toUnifiedAddress(){
StringBuffer buffer = new StringBuffer();
buffer.append("(");
buffer.append(PATTERN_ADDRESS_UM_MYUN_DONG_GA_RI);
buffer.append(PATTERN_ADDRESS_BUNJI);
buffer.append(")");
buffer.append("|");
buffer.append("(");
buffer.append(PATTERN_ADDRESS_UM_MYUN_DONG_GA_RI);
buffer.append(PATTERN_ADDRESS_SAN_BUNJI);
buffer.append(")");
return buffer.toString();
}
}
코드를 읽을 때 알아 둘 점
한글이 전부 \uC74D 같은 유니코드 이스케이프로 적혀 있어서 처음 보면 무슨 글자인지 알기 어렵습니다. 읽기 쉽게 풀면 \uC74D=읍, \uBA74=면, \uB3D9=동, \uC2DC=시, \uAD70=군, \uAD6C=구, \uB3C4=도, \uAC00=가, \uB9AC=리, \uC0B0=산, \uB85C=로, \uAE38=길, \uC9C0\uD558=지하, \uBC88\uC9C0=번지, \uC8FC·\uC0AC·\uC7AC=주·사·재입니다. 소스 파일 인코딩이 서버마다 달라 한글이 깨지는 걸 피하려고 이렇게 쓰는 경우가 많습니다.
PATTERN_HANGUL의 [\\x{ac00}-\\x{d7af}]는 완성형 한글 음절 범위(가~힣 부근)를 뜻합니다. \x{...} 형태는 자바 7부터 지원하니, 더 오래된 환경이라면 [\uAC00-\uD7AF]로 바꿔야 할 수도 있습니다.
전화번호 패턴은 02-472-2012, (02)4722012, 하이픈 없는 9~11자리 숫자를 모두 잡도록 앞쪽 괄호와 하이픈을 ?로 선택 처리했습니다. 반대로 말하면 주민번호 앞자리나 주문번호처럼 우연히 9~11자리인 숫자도 걸린다는 뜻이라, 실제 데이터에 적용할 때는 앞뒤에 경계 조건을 추가하거나 결과를 한 번 더 걸러 내는 편이 안전합니다.
toSidoSigunguRoad() 같은 메서드는 상수들을 순서대로 붙여 "시도 + 시군구 + 도로명 + 건물번호"를 한 번에 찾는 패턴을 돌려줍니다. 각 조각 사이에 공백 허용이 없어서, 공백을 미리 PATTERN_BLANK로 지운 문자열에 적용하는 흐름을 전제로 한 것으로 보입니다. 사용할 때는 Pattern.compile(PatternDefine.toRoad())로 한 번만 컴파일해 두고 Matcher를 돌리는 식으로 쓰면 됩니다. 매번 문자열에서 컴파일하면 데이터가 많을 때 눈에 띄게 느려집니다.
참고: 정규식 패턴 설명 및 예제 - 잘해보자구!