메뉴 바로가기 검색 및 카테고리 바로가기 본문 바로가기

한빛출판네트워크

IT/모바일

정규 표현식을 잡아라!

한빛미디어

|

2003-03-21

|

by HANBIT

12,365

저자: 한빛리포터 김영익

이 기사에서는 간단한 예제를 통해 자바에서 문자열 처리의 팔방미인으로 불리는 정규 표현식 사용법을 살펴볼 것이다. 정규 표현식은 JDK 1.4에서부터 새로 지원하는 기능으로 문자열 처리 작업을 많이 하는 자바 개발자에게는 훌륭한 도구가 될 것이다.

정규 표현식(Regular Expression)

정규 표현식은 텍스트와 데이터를 조작하는데 사용하는 강력한 문자열 처리 도구이다. 주로 유닉스와 펄을 사용하는 사람들에게는 아주 익숙한 도구이겠지만 자바에서는 그동안 지원하지 못하고 있었다. 따라서 자바 개발자들은 별도의 다른 라이브러리를 구해서 사용하거나 직접 기능을 구현하는 수고를 해왔으나, 1.4 버전부터는 정규 표현식 패키지가 추가되어 이러한 번거로움을 덜 수 있게 되었다. 아직 정규 표현식이 무엇인지 어디에 사용해야 하는지 감이 안온다면 아래의 참고 도서 『정규 표현식 완전 해부와 실습(개정판)』을 보기 바란다. 그리고 유닉스나 리눅스 환경을 많이 접하기를 추천한다.


정규 표현식 완전 해부와 실습(개정판)

참고 도서

정규 표현식 완전 해부와 실습(개정판)
제프리 프리들(Jefferey E. F, Friedl)




앞으로 정규 표현식을 사용하는 예제 프로그램들이 등장한다. 정규 표현식 자체에 대한 자세한 사항이나 자바 프로그램의 기초에 대해서는 다른 서적을 참고하기 바란다. 우리가 주목해야 할 것은 정규 표현식을 어떻게 자바에서 사용하며, 어떤 예제들이 가능한지 살펴보는 것이다. 또한 예제들의 구현은 정규 표현식을 사용할 경우와 사용하지 않을 경우를 비교할 수 있도록 하였다.

† 저자 주: 예제 작성과 실행은 윈도우 XP에서 이루어졌다. 윈도우를 제외한 다른 운영체제에서도 작동하겠지만 확인해보지 못했다. 모든 예제는 본인이 임의로 작성한 것이다. 당연히 최적화가 되어 있지 않으며 다른 방법으로 구현할 수도 있다. 성능 비교 부분에서도 표현식이나 구현 방법, 입력 데이터 값 등의 요인에 의해서 결과는 아주 다를 수 있음을 밝혀둔다.

첫 번째 임무: 빈칸을 모두 제거하라!

주소록 프로그램을 작성한다고 상상해보자. 우리가 사용하는 우편번호는 "123-456" 이런 형태로 이루어져 있다. 물론 중간에 있는 "-" 문자는 미리 제거하거나 형식을 지정해서 입력 받으면 입력 오류를 상당히 줄일 수 있다. 그러나 주소를 입력하던 도중 실수로 스페이스바를 잘못 눌러 빈칸이 포함되게 되는 경우, 이 빈칸이 반드시 데이터에 필요한지 아니면 실수로 입력 된 것인지 알기 어렵다. 빈칸 없이 입력되어야 하는 데이터에서는 당연히 빈칸은 제거되어야 할 대상이다.

본인이 수행하는 프로젝트에서는 다음과 같은 조건이 있었다.
"입력되는 스트링은 통신 장비들 사이의 구성 관계 트리를 나타내는 값이다"
"각 노드의 구분은 슬래시(/)로 구분된다"
"스트링 사이에 빈칸은 존재해서는 안된다"
예: "cen99/con06/rnc06/bts00/BCCA00"
그런데 문제는 입력하는 사람들의 실수로 빈칸이 입력되는 경우가 많다는 것이었다. 그래서 결국 빈칸을 없애는 코드를 다음과 같이 작성하게 되었다.

정규 표현식을 사용하는 경우
public static String removeAllChar(String in, char c) {
        String regex = "\\"  + new Character(c).toString();
        return in.replaceAll(regex, ""); // since JDK 1.4
}
처음부터 끝까지 문자 c로 매치되는 것을 스트링 ""으로 변환하는 코드이다. 따라서 모든 문자 c가 제거되는 효과를 나타낸다. 테스트 코드는 다음과 같다.
// 고의로 중간에 빈칸이 삽입된 스트링
String s1 = " cen99/con06 /rnc06/bts00/BCCA00 ";
String r1 = removeAllChar(s1, " ");
System.out.println("before removeAllChar() => [" + s1 + "]");
System.out.println("after  removeAllChar() => [" + r1 + "]");
결과는 다음과 같은 형태로 나타난다.
…
before removeAllChar() => [ cen99/con06 /rnc06/bts00/BCCA00 ]
after  removeAllChar() => [cen99/con06/rnc06/bts00/BCCA00]
…
정규 표현식을 사용하지 않는 경우
public String removeAllChar2(String in, char c) {
StringBuffer buffer = new StringBuffer();
        for (int i = 0 ; i < in.length() ; i++) {
            if (in.charAt(i) != c) {
                buffer.append(in.charAt(i));
            }
        }
        return buffer.toString();
}
정규 표현식을 사용하지 않고 StringBuffer 클래스를 사용하였다. 코드는 좀더 복잡해 보이지만 결과는 동일하다.

성능 비교

정규 표현식의 사용 유무는 결과만을 두고 비교할 때는 중요하지 않을 수도 있다. 어차피 같은 입력을 넣었을 때 원하는 결과가 나온다면 만족스러울 것이다. 그러나 어떤 코드가 더 빠르게 수행될 것인가? 같은 작업을 수천번, 수만번 반복한다면 빠른 코드를 작성해야 하는 것이 당연한 이치일 것이다. 두 경우를 간단히 성능 비교를 해본 결과를 살펴보자. 수행 시간은 단순히 메소드 호출 전의 시간과 메소드 호출 후의 시간 차이이다. 정확한 비교를 위해서는 다른 도구를 사용하거나 아주 많은 테스트를 거쳐야 하겠지만 시간 관계상 2번씩 수행하고 평균을 낸 결과는 다음과 같았다. 코드를 눈으로 보았을 때에는 정규 표현식을 사용하지 않은 경우의 코드가 복잡하므로 훨씬 수행 시간이 길거라는 예상이었다. 그러나 결과는 ???


[그림 1] 수행시간 비교

놀랍게도 결과는 정반대였다. 정규 표현식을 사용한 간결한 코드가 더 오래 걸리는 것이다. 그래프를 간단히 설명하자면 다음과 같다. 세로축은 메소드 수행시간을 밀리세컨드 단위로 측정한 것이고, 가로축은 메소드의 입력으로 넘겨지는 스트링의 길이를 지정하는 for 문의 인덱스(i)이다. 그래프 결과를 만든 소스 코드의 일부를 살펴보자.
…
String s1 = "";
for (int i = 0 ; i < 2000 ; i++) {
	// 테스트를 위해서 일부러 빈칸을 넣어 보았다.
s1 = s1 + " cen99/con06 /rnc06/bts00/BCCA00 "; 
}

startTime = System.currentTimeMillis(); // 시작 시간
String r1 = removeAllChar(s1, " ");
endTime = System.currentTimeMillis(); // 종료 시간
System.out.println(endTime - startTime); // 수행 시간
…
입력되는 스트링의 값이나 길이, 표현식에 의해서 결과는 다르게 나올 수 있다. 여러분도 다른 방법으로 성능을 비교하면서 결과를 예상해보기 바란다.

두 번째 임무 : 상위 경로를 제거하라!

우리가 사용하는 윈도우나 유닉스 등의 운영체제에서는 파일 시스템을 제공한다. 영속적으로 보존해야 하는 모든 데이터는 파일에 저장해야한다고 할 수 있으므로, 그만큼 파일은 중요하다. 따라서 모든 운영체제와 프로그램 언어에서 파일 다루는 방법을 제공한다. 이번 예제는 이런 파일 시스템을 다루는 기능의 일부로서 요구사항은 아주 간단하다.
"파일의 절대 경로에서 상위 경로를 제외한 파일이나 디렉토리 자체의 이름만을 가져와라"
즉, "C:\wos\config\aokey\trace030217.log"와 같은 파일의 경로가 있을 경우 원하는 결과는 "trace030217.log" 이다.

정규 표현식을 사용하는 경우
1: public String removeParentPath(String in) {
2:         String os = System.getProperty("os.name");
3:  	   String regex = null;
4:         if (os.startsWith("SunOS")) { // Solaris
5:             regex = "^.*/";
6:         }
7:         else if (os.startsWith("Windows")) { // Windows
8:             regex = "^.*\\\\";
9:         }
10:        else {
11:            // Unknown os
12:            return null;
13:        }
14:        in = in.replaceFirst(regex, "");
15:        return in;
16: }
예제에 사용되는 정규 표현식에 대해 궁금하다면 참고 도서 『정규 표현식 완전 해부와 실습(개정판)』의 274페이지를 참고하기 바란다. 주의 할 사항은 5, 8번째 줄에서 운영체제에 따라 파일과 디렉토리를 구분하는 문자가 다르다는 것이다. 실행을 위해서 아래와 같은 코드를 작성했다.
String s3 = "C:\\wos\\config\\aokey\\trace030217.log";
String r3 = removeParentPath(s3);
System.out.println("before => [" + s3 + "]");
System.out.println("after => [" + r3 + "]");
결과는 다음과 같은 형태로 나타난다.
…
before => [C:\wos\config\aokey\trace030217.log]
after => [trace030217.log]
…
정규 표현식을 사용하지 않는 경우
public String removeParentPath(String in) {
        String separatorChar = System.getProperty("file.separator");
        return in.substring(in.lastIndexOf(separatorChar)+1 , in.length());
}
이번에는 정규 표현식을 사용하지 않는 경우의 코드가 훨씬 간결해 보인다. 물론 결과는 동일하다. 이번 예제의 성능 비교는 여러분이 직접 해보길 바란다. 이번에도 정규 표현식을 사용하지 않은 경우가 더 빠르리라 예상된다.

마치면서

JDK 1.4에서 정규 표현식을 사용하는 예제를 간단히 살펴 보았다. 정규 표현식은 워낙 훌륭한 도구이기 때문에 적절히 사용하면 아주 많은 분야에서 활용이 가능하다. 실제로 프로젝트 수행에 사용되는 코드만을 예로 들었기 때문에 나름대로 충실하리라 예상하지만 본인도 아직 정규 표현식을 많이 사용해보지 못해서 여러분이 느끼기에 소개된 예제가 부족한 느낌이 있거나 적절하지 못할 수 도 있다. 정규 표현식을 사용하는 경우와 사용하지 않는 경우의 두 가지 예를 함께 성능 비교를 한 이유는, 여러분이 고정관념에 사로잡히지 않았으면 하는 이유에서였다. 표현식, 입력되는 데이터 값, 또는 주변 상황에 따라 비교 결과는 얼마든지 변할 수 있는 것이다. 단지 여러분은 모든 방법을 알고, 빠른 방법을 선택하기만 하면 될 것이다.

참고문헌
TAG :
댓글 입력
자료실

최근 본 상품0