IBM Korea Skip to main content
       IBM 홈    |  제품 & 서비스  |  고객지원 & 다운로드  |  회원가입  

정규식을 위한 추상 자바 API 구현하기
Perl5 regexp 라이브러리를 사용하기 위해 일반적인 API 만들기

Level: Intermediate

Jose San Leandro Armendariz
Independent Software Engineer
2002년 12월 1일

자바에서 정규식을 이용하여 작업할 때 구체 (concrete) regexp 라이브러리에 의존하는 것은 좋은 생각이 아니다. 추상(abstract) 레이어를 사용한다면 다른 regexp 라이브러리들을 변환할 수 있고 코드와 특정 라이브러리 사이의 커플링을 줄이며, 가장 알맞은 것을 선택할 수 있다.

Introduction
텍스트를 분석하는 자바 애플리케이션을 만드는 것이 쉽다고 생각하지만 이 일은 금새 복잡해질 수도 있다. 이것은 HTML 페이지를 파싱하는 코드를 작성할 때의 내 경험이기도 했다. "가끔씩" Perl5 정규식(regexps)을 사용하다가 이제는 "자주" 사용하게 되었다. 이것이 바로 이 글을 쓰는 이유이다.

배경
내 경험으로 비춰볼 때 대부분의 자바 개발자들은 몇 가지 종류의 텍스트를 파싱해야한다. 일반적으로 자바 스트링 관련 함수나 indexOf 또는 substring과 같은 메소드로 초기에 작업을 해야한다는 것을 의미한다. 인풋 포맷이 전혀 바뀌지 않을 것을 기대하면서 말이다. 하지만 인풋 포맷이 변경되면 새로운 포맷을 읽는 코드는 점점 복잡해지고 유지가 어려워진다. 결과적으로 코드는 단어 래핑, 케이스 민감도 등등을 지원해야한다.

로직이 점점 복잡해짐에 따라 관리는 힘들어진다. 어떤 변화든 부작용을 낳고 텍스트 파서의 다른 부분들이 작동 정지를 유발하기 때문에 개발자들은 작은 버그라도 픽스 할 시간이 필요하다.

펄 경험이 있는 개발자는 이미 정규식도 익숙할 것이다. 그런 개발자가 있다면 이러한 기술을 사용할 때 다른 팀원들에게 안정감을 줄 수 있다. 이 새로운 접근방식은 핵심 파서 로직을 삭제하고 여기에 regexp 라이브러리를 대체시키는 것이다.

Perl5에 숙련된 개발자의 제안을 받아들이면 그 팀은 어떤 regex 구현이 프로젝트에 가장 잘 맞을 것인가를 선택 해야한다. 그런다음 이를 어떻게 사용하는지를 배워야한다.

인터넷에서 다양한 대안들을 연구한 후에, 팀은 아마도 Oro 같은 잘 알려진 라이브러리들 중 하나를 선택한다. 그런다음 파서는 힘겹게 리팩토링되거나 거의 다시 작성되거나 결국 Perl5Compiler, Perl5Matcher 등의 Oro 클래스를 사용하게 된다.

이러한 선택의 결과는 명확하다:

  • 이 코드는 Jakarta Oro의 클래스에 강결합된다.

  • 이 팀은 비함수적 요구사항이 충족될 것인지의 여부를 몰라 위험을 안게된다.

  • 이 팀은 코드를 배우고 재작성하는데 시간과 돈을 투자하여 regexp 라이브러리를 사용한다. 그들의 결정이 틀리다면 새로운 라이브러리가 선택된다. 이렇게 한다고 해서 비용상 큰 차이는 없는데 이유는, 코드는 다시 재작성되어야 할 것이기 때문이다.

  • 라이브러리가 잘 작동함에도 불구하고 새로운 것으로 마이그레이션 해야하는가?

결합방지(decoupling)의 이점
현재 뿐만 아니라 앞으로도, 팀의 필요에 가장 맞는 구현이 어떤 것인지를 알 수 있는 방법은 있는가? 해답을 함께 찾아보도록 하자.

특정 구현에 대한 의존성을 피할 것
앞의 이야기는 소프트웨어 엔지니어링 분야에서는 매우 일반적이다. 어떤 경우 그와 같은 상황은 많은 투자와 오랜 지연을 야기시킬 수 있다. 일반적으로 모든 결과를 예견하지 않고 결정이 이루어지거나 결정 주체자가 이와 관련된 경험이 없을 때 발생한다.

다음과 같이 상황을 요약할 수 있다:

  • 특정한 유형의 프로바이더가 필요하다.
  • 최상의 프로바이더가 어떤 것인지를 선택할 객관적인 기준이 없다.
  • 최소의 비용으로 모든 후보를 평가할 수 있기를 원한다
  • 이 결정은 선택된 프로바이더와 당신을 결속시켜서는 안된다.

이러한 문제에 대한 솔루션은 프로바이더에 독립적인 코드를 만드는 것이다. 여기에 새로운 레이어가 도입되는데, 그것은 클라이언트와 프로바이더의 결합을 방지한다.

서버측 개발에서 이러한 접근방식을 사용하는 패턴이나 아키텍쳐를 찾기는 쉽다. 몇 가지 예를 인용하면 다음과 같다:

  • J2EE를 이용하여 애플리케이션 서버 상세 보다는 애플리케이션을 구현하는데 초점을 맞춘다.
  • Data Access Object (DAO) 패턴은 당신이 데이터베이스에 접근하는 방식의 상세와 복잡함을 숨긴다. 왜냐하면 이것은 추상 영속성 레이어에 액세스하는 방식을 제공하고 데이터가 실제로 저장되는 클라이언트 코드 내의 데이터베이스 이슈를 처리해야 할 필요성을 경감해주기 때문이다. 이것은 Gang of Four (GoF) 패턴이 아니며 Sun의 J2EE best practices의 일부이다.

가상의 개발 팀 예제에서 그들은 다음과 같은 레이어를 찾고있다:

  • 모든 정규식 구현 뒤에 개념을 추상화 시킨다. 이렇게 되면, 팀은 그와 같은 개념을 배우고 이해하는데 초점을 맞출 수 있다. 그들이 배우는 것은 모든 애플리케이션 또는 모든 버전에 적용될 수 있다.

  • 부작용 없이 새로운 라이브러리를 지원한다. 플러그인 아키텍쳐에 기반하여 regexp 패턴을 실행하는 실제 라이브러리는 동적으로 선택되고 어댑터는 결합되지 않는다. 새로운 라이브러리는 새로운 어댑터의 필요성만을 드러낸다.

  • 다른 대안들과 비교할 수 있는 방식을 제공한다. 간단한 벤치마크 유틸리티는 재미있는 퍼포먼스 측정을 보여줄 수 있다. 그들이 각각의 구현용 유틸리티를 실행한다면 팀은 가치있는 정보를 얻고 최상의 선택을 할 수 있다.

좋은 생각이다..하지만..
어떤 결합방지 접근방식이든 최소한 하나의 단점을 갖고 있다. 클라이언트 코드가 단지 하나의 구현에서 제공하는 특정 기능을 필요로 한다면 어떻게 하겠는가? 다른 구현을 사용할 수 없기 때문에 당신의 코드를 결합해야한다. 앞으로 향상되겠지만 지금으로선 방법이 없다.

이러한 예는 흔하다. regexp 세계에서 특정 구현에 의해서만 지원되는 몇 가지 컴파일러 옵션이 있다. 당신의 클라이언트 코드가 이러한 특정 기능을 필요로 한다면 이 일반적인 레이어로는 부족하다.

추가 레이어가 각 구현의 특정 기능을 지원하고 이를 지원하니 않는 것이 선택되면 예외를 주어야하는가? 그것은 솔루션이 될 수 있지만 일반적인 추상 개념을 정의하는 원래의 목적은 아니다.

하나의 GoF 패턴이 이러한 상황에 완벽하게 맞을 수 있다: 책임 사슬(Chain of Responsibility). 이것은 디자인에 또 다른 대안을 제시했다. 이러한 접근방식으로 클라이언트 코드는 메시지나 명령어를 그와 같은 메시지를 처리할 수 있는 엔터티 리스트에 보낸다. 이 리스트 아이템은 체인(사슬)로 짜여져서, 메시지는 순서대로 처리되고 체인의 끝에 다다르기 전에 소비될 수 있다.

이 경우 특정 구현에 의해서만 지원되는 구현이 특별한 유형의 메시지에 의해 모델링 될 수 있다. 아이템이 그와 같은 기능을 인식하는지에 따라 이 메시지를 다음 것에 전달할지의 여부는 그 체인의 아이템에 달려있다.

일반적인 API 정의하기
여기에서 설명한 API는 RegexpPlugin 이다. regexp 라이브러리와 코드 사이의 결합방지를 지원한다.

RegexpPlugin
API 다음 예제에서 구체 구현(Jakarta Oro)과 RegexpPlugin 사용의 차이점을 요약하겠다.

매우 간단한 regexp으로 시작하겠다: 파싱해야하는 텍스트가 사람의 이름이라고 상상해보자. 당신이 받는 포맷은 John A. Smith 와 같을 것이고 이름(John)만을 취하고 싶다. 하지만 단어가 스페이스, 라인 브레이크, 탭, 또는 이들의 조합으로 분리되는지의 여부를 모른다. 그와 같은 인풋 포맷을 처리할 수 있는 regexp은 .*\s*(.*?)\s+.* 뿐이다.

regexp은 다음과 같다: 스페이스 앞에 오는 첫 번째 텍스트를 취하라. 자바 코드를 작성해보자.

핸즈온
자바 코드 내의 정규식을 사용하려면 다음의 7 단계를 완성해야한다:

Step 1: 컴파일러 인스턴스를 만든다. Jakarta Oro를 사용하여 Perl5Compiler를 인스턴스화 해야한다:


org.apache.oro.text.regex.Perl5Compiler compiler =
    new org.apache.oro.text.regex.Perl5Compiler();

RegexpPlugin를 사용하는 동일한 코드도 비슷하다:


org.acmsl.regexpplugin.Compiler compiler =
    org.acmsl.regexpplugin.RegexpManager.createCompiler();

차이점은 있다. 앞서 언급한것 처럼 이 API는 실제로 사용되는 구체 구현이 어떤 것인지를 숨긴다. 구체 구현을 선택하거나 디폴트 Jakarta Oro를 남겨둘 수 있다. 선택된 라이브러리를 런타임시 사용할 수 없다면 RegexpPlugin API는 이 클래스 이름을 사용하여 컴파일러를 만들기를 시도한다. 그 작동이 실패하면 예외를 API의 클라이언트로 되돌린다.

JDK 1.4의 빌트인 regexp 클래스를 사용한다고 가정해보자. 그렇다면 전혀 사용되지 않을 추가의 jar 파일을 포함하는 어떤 포인트도 없다. createCompiler() 메소드를 호출하는 것으로는 충분하지 않기 때문이다. 선택된 라이브러리가 존재하지 않을 때마다 예외를 관리해야한다. 이 예제는 업데이트 되었어야 했다:


try
{
    org.acmsl.regexpplugin.Compiler compiler =
        org.acmsl.regexpplugin.RegexpManager.createCompiler();
}
catch (org.acmsl.regexpplugin.RegexpEngineNorFoundException exception)
{
    [..]
}

Step 2: regexp 패턴을 컴파일 한다. 정규식은 Pattern 객체로 컴파일 된다.


org.apache.oro.text.regex.Pattern pattern =
    compiler.compile(".*\\s*(.*?)\\s+.*", Perl5Compiler.MULTILINE_MASK);

슬래시 (\) 문자를 없애야한다는 것을 주목하라.

이 패턴 객체는 텍스트 포맷에서 정의된 정규식을 나타낸다. 가능하면 패턴 인스턴스를 재사용하라. regexp이 픽스되면 이 패턴은 클래스에서 정적 멤버가 될 것이다.

compile 메소드는 EXTENDED_MASK 같은 플래그로 설정되기에 알맞다. 하지만 RegexpPlugin은 임의의 플래그를 허용하지 않는다. 유일하게 지원되는 것은 case sensitivitymultiline이다.

컴파일러 인스턴스는 특정 속성을 갖고 있어 그와 같은 플래그를 정의한다:


compiler.setMultiline(true);

org.acmsl.regexpplugin.Pattern pattern =
    compiler.compile(".*\\s*(.*?)\\s+.*");

Step 3: Matcher 객체를 만든다. Jakarta Oro에서 이 단계는 매우 간단하다:


org.apache.oro.text.regex.Perl5Matcher matcher =
    new org.apache.oro.text.regex.Perl5Matcher();

구현되어야 할 정보가 필요없기 때문에 매우 간단하다:

Step 4: 정규식을 평가한다. matcher 객체는 정규식을 인터프리팅하고 필요한 정보를 추출해야한다:


if (matcher.contains("John A. Smith", pattern))
{

인풋 텍스트가 정규식과 맞으면 이 메소드는 true를 리턴한다.

RegexpPlugin API를 사용하면 이 모든 부분에서 차이점이 없다.

Step 5: 첫 번째 match를 검색한다. 이 간단한 단계는 단 한줄로 이루어진다:


    org.apache.oro.text.regex.MatchResult matchResult = matcher.getMatch();

로컬 변수를 선언하여 regexp과 매치하는 텍스트를 갖고있는 객체를 저장한다. 두 경우 모두 이 단계는 같다. 변수 선언을 제외하면..:


    org.acmsl.regexpplugin.MatchResult matchResult =
        matcher.getMatch();

Step 6: 관심있는 group을 선택한다. 두 가지 접근 방식이 있다:

  • 구체(concrete) 라이브러리
  • RegexpPlugin API

regexp이 .*\s*(.*?)\s+.*>이기 때문에 단지 한 그룹인 (.*?)만을 갖는다.

MatchResult 객체에는 모든 그룹이 순서대로 포함되어 있다. 얻고자 하는 그룹의 위치를 알아두면 된다:


    String name = matchResult.group(1);

    [..]
}

변수인 name 에는 텍스트 John 이 포함되어 있는데 이것은 정확히 우리가 원한 그것이다.

Step 7: 필요하다면 프로세스를 반복한다.Step 7: 필요하다면 프로세스를 반복한다.


while (matcher.contains("John A. Smith", pattern))
{

매핑(Mapping)
일반적인 추상 API를 작성하는 것 이외에도 자바 환경에서 이미 존재하는 regexp 엔진에 대한 어댑터를 구현해야한다.

다음 테이블은 하나의 라이브러리에서 다른 라이브러리로 마이그레이션 하는 방법을 설명하고 있다. 어떤 개념은 확실하지 않다.

Regexp concept GNU Regexp 1.2
Compiler gnu.regexp.RE
Pattern gnu.regexp.RE
Matcher gnu.regexp.REMatchEnumeration
gnu.regexp.RE
Match result gnu.regexp.REMatch
Malformed pattern exception gnu.regexp.REException

Regexp concept Jakarta Oro 2.0.6
Compiler org.apache.oro.text.regex.Perl5Compiler
Pattern org.apache.oro.text.regex.Pattern
Matcher org.apache.oro.text.regex.Perl5Matcher
Match result org.apache.oro.text.regex.MatchResult
Malformed pattern exception org.[..].regex.MalformedPatternException

Regexp concept Jakarta Regexp 1.3
Compiler org.apache.regexp.RE
org.apache.regexp.RECompiler
org.apache.regexp.REProgram
Pattern org.apache.regexp.REProgram
org.apache.regexp.RE
Matcher org.apache.regexp.RE
org.apache.regexp.REProgram
Match result org.apache.regexp.RE
Malformed pattern exception org.apache.regexp.RESyntaxException

Regexp concept JDK 1.4 regex package
Compiler java.util.regex.Pattern
Pattern java.util.regex.Pattern
Matcher java.util.regex.Matcher
Match result java.util.regex.Matcher
Malformed pattern exception java.util.regex.PatternSyntaxException

벤치마크(Benchmark)

테스트를 위해 개발된 벤치마킹 유틸리티는 HTML 파서를 사용하여 웹 콘텐트를 처리하면서 링크, 형식, 테이블 등에 대한 정보를 업데이트한다. 하지만 중요한것은 파싱 로직이 정규식에 위임되었고 따라서 RegexpPlugin API를 통해 이루어진다.

이 벤치마크는 매우 단순한 HTML 페이지를 천 번 파싱하는 것으로 구성된다. 결과는 다음과 같다.

Regexp library 벤치마크 결과 (초)
Jakarta Oro 2.0.6 130,71
Jakarta Regexp 1.2 23,261
GNU Regexp 1.1.4 1,966.939
JDK1.4 33,222

실제 애플리케이션에서는 여러가지 방식으로 퍼포먼스를 향상시킬 수 있다. regexp 라이브러리로 작업할 때 매번 패턴들을 컴파일 할 필요가 없다는 것이 가장 중요하다. 대신 그들을 컴파일하고 각각의 인스턴스들을 재사용 할 수 있다. 하지만 regexp이 픽스되지 않으면 컴파일 작업은 무시된다.

요약
정규식 파서는 강력하다. 팀이 안정되고 파싱 로직이 발전하면 관리가 수월하다. 하지만 개발자들은 regexp 신택스를 이해하여 그와 같은 코드가 어떻게 작동되는지를 알아야한다.

참고자료

목 차:
Introduction
배경
결합방지(decoupling)의 이점
특정 구현에 대한 의존성을 피할 것
좋은 생각이다..하지만..
일반적인 API 정의하기
RegexpPlugin
핸즈온
매핑(Mapping)
벤치마크(Benchmark)
요약
관련 dW 링크:
Magic with Merlin: Parse sequences of characters with the new regex library
XP Distilled: Greater success on your Java projects
Demystifying Extreme Programming, part 1
Demystifying Extreme Programming, part 2
Subscribe to the developerWorks newsletter
US 원문 읽기
Also in the Java zone:
Tutorials
Tools and products
Code and components
Articles
필자소개
Jose San Leandro Armendariz J2EE 프로젝트에 많은 경험이 있다.
이 기사에 대하여 어떻게 생각하십니까?

정말 좋다 (5) 좋다 (4) 그저그렇다 (3) 수정보완이 필요하다(2) 형편없다 (1)

  회사소개  |  개인정보 보호정책  |  법률  |  문의