주소나 연락처가 섞인 텍스트에서 원하는 부분만 뽑아야 할 때 정규식만 한 게 없습니다. 엑셀에서 붙여 넣은 고객 목록, 크롤링한 업체 정보, 사람이 손으로 입력한 주소 칸 같은 데이터는 형식이 제각각이라 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를 돌리는 식으로 쓰면 됩니다. 매번 문자열에서 컴파일하면 데이터가 많을 때 눈에 띄게 느려집니다.

참고: 정규식 패턴 설명 및 예제 - 잘해보자구!