상세 컨텐츠

본문 제목

닷넷(.NET)에서의 정규표현식 사용하기 Part 3

C#

by 탑~! 2014. 1. 3. 08:41

본문

Part1, 2에서 정규표현식을 어떻게 사용하는지에 대해 알아봤습니다. 이 글에서는 Regex 클래스를 사용하는 다양한 방법들에 대해 성능테스트를 해 보고 어떻게 하면 최적의 성능으로 정규표현식을 사용할 수 있는지에 대해 알아보도록 하겠습니다.

 

Regex 클래스와 정규표현식 패턴을 사용하여 개발을 해 본 개발자들은 수행성능에 대해 불평을 하곤 합니다. 왜 이렇게 느린 거야? 라고 말입니다. 하지만 왜 성능이 좋지 않은지 쉽게 진단할 수 없습니다. 정규표현식의 수행속도가 느린 이유는 크게 두 가지로 볼 수 있습니다. 첫 번째는 당연히 정규표현식 패턴 자체에 문자가 있는 경우며, 두 번째는 Regex 클래스를 사용하는 방식이 잘못 되었기 때문입니다. .NET 프레임워크의 정규표현식 개체 모델의 핵심은 Regex 클래스입니다. 다시 말해, Regex 엔진을 사용하는 방식이 정규표현식의 성능에 영향을 주는 핵심 요인인 것입니다.

 

정규표현식 패턴 자체에 대한 성능은 그 분량이 방대하여 이 글에서 다루지 않으며 정규표현식 관련 서적을 참고 하십시오.

 

인스턴스 메서드와 정적 메서드의 성능 차이

Regex 클래스의 인스턴스 메서드를 호출하고 싶다면, 반드시 Regex 클래스의 인스턴스를 생성해야 합니다. 이전 강좌에서 설명하였듯이 Regex 클래스의 인스턴스를 생성 할 때는 생성자 매개변수로 정규표현식 패턴을 전달해야 합니다. 하지만 Regex 개체가 한번 생성되면 생성자 매개변수에 지정된 정규표현식을 변경할 수 없습니다. 이는 강력하게 결합(tightly coupling)되어 있다고 말할 수 있으며, 새로운 정규표현식을 사용하기 위해서는 새로운 Regex 개체를 생성해야만 합니다.

 

이 문제를 해결하기 위해 Regex 클래스는 정적 메서드를 제공하고 있습니다. 아시다시피 정적 메서드는 인스턴스를 생성할 필요가 없으므로 불필요한 인스턴스의 생성으로 인한 비용을 줄일 수 있게 됩니다.

 

static void Run2(string pUrl)

{

   //비교 대상 입력 자료

    string[] urls = { 

                        "http://taeyo.net",

                        "http://www.taeyo.net",

                        "http://www.taeyo.net/board",

                        "http://taeyo.net/board/board.aspx",

                        "http://taeyo.net/talk/board.aspx?id=12",

                        "http://taeyo.net&",

                        "http://www.taeyo.net%",

                        "http://www.taeyo.net/board@",

                        "http://dayofdays.net",

                        "http://blog.dayofdays.net",

                        "http://blog.dayofdays.net/article",

                        "http://dayofdays.net/article/article.aspx?category=test",

                        "http://dayofdays.net/test-url"                        

                    };

   //인스턴스 메서드를 사용하는 테스트

    Stopwatch sw = Stopwatch.StartNew();

    foreach (string url in urls)

    {

        if (IsValidUrlWidthInstance(pUrl, url))

        {

            Console.WriteLine("{0} is valid", url);

        }

        else

        {

            Console.WriteLine("{0} is not valid", url);

        }

    }

    sw.Stop();

    Console.WriteLine("Instance Elapsed Time :\t{0}", sw.Elapsed);

 

   //정적 메서드를 사용하는 테스트

    sw = Stopwatch.StartNew();

    foreach (string url in urls)

    {

        if (IsValidUrlWidthStatic(pUrl, url))

        {

            Console.WriteLine("{0} is valid", url);

        }

        else

        {

            Console.WriteLine("{0} is not valid", url);

        }

    }

    sw.Stop();

    Console.WriteLine("Static Elapsed Time :\t{0}", sw.Elapsed);

}

 

//Regex클래스의 인스턴스 생성

static bool IsValidUrlWidthInstance(string pUrl, string url)

{

    Regex regex = new Regex(pUrl);

    return regex.IsMatch(url);

}

//Regex 클래스의 정적 메서드 호출

static bool IsValidUrlWidthStatic(string pUrl, string url)

{

    return Regex.IsMatch(url, pUrl);

}

 

static void Main(string[] args)

{

   //단순한 정규표현식

    string pUrl1 = @"^https?://([\w-]+\.)+[\w-]+(/[\w-./?&%=]*)?$";

   //복잡한 정규표현식

    string pUrl2 = @"^(http|https|ftp)\://([a-zA-Z0-9\.\-]+(\:[a-zA-Z0-9\.&%\$\-]+)*@)?((25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9])\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9]|0)\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9]|0)\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[0-9])|([a-zA-Z0-9\-]+\.)*[a-zA-Z0-9\-]+\.[a-zA-Z]{2,4})(\:[0-9]+)?(/[^/][a-zA-Z0-9\.\,\?\'\\/\+&%\$#\=~_\-@]*)*$";

    Console.WriteLine("1. 간단한 정규표현식---------------");

    Run2(pUrl1);

    Console.WriteLine("2. 복잡한 정규표현식---------------");

    Run2(pUrl2);

}

 

위 코드는 단순한 정규표현식과 복잡한 정규표현식을 각각 사용하는 인스턴스 생성 방식과 정적 메서드 호출 방식을 테스트 하기 위한 코드입니다. 실행결과는 다음과 같습니다.

 

1. 간단한 정규표현식---------------

http://taeyo.net is valid

http://www.taeyo.net is valid

http://www.taeyo.net/board is valid

http://taeyo.net/board/board.aspx is valid

http://taeyo.net/talk/board.aspx?id=12 is valid

http://taeyo.net& is not valid

http://www.taeyo.net% is not valid

http://www.taeyo.net/board@ is not valid

http://dayofdays.net is valid

http://blog.dayofdays.net is valid

http://blog.dayofdays.net/article is valid

http://dayofdays.net/article/article.aspx?category=test is valid

http://dayofdays.net/test-url is valid

Instance Elapsed Time : 00:00:00.0022522

http://taeyo.net is valid

http://www.taeyo.net is valid

http://www.taeyo.net/board is valid

http://taeyo.net/board/board.aspx is valid

http://taeyo.net/talk/board.aspx?id=12 is valid

http://taeyo.net& is not valid

http://www.taeyo.net% is not valid

http://www.taeyo.net/board@ is not valid

http://dayofdays.net is valid

http://blog.dayofdays.net is valid

http://blog.dayofdays.net/article is valid

http://dayofdays.net/article/article.aspx?category=test is valid

http://dayofdays.net/test-url is valid

Static Elapsed Time :   00:00:00.0029799

2. 복잡한 정규표현식---------------

http://taeyo.net is valid

http://www.taeyo.net is valid

http://www.taeyo.net/board is valid

http://taeyo.net/board/board.aspx is valid

http://taeyo.net/talk/board.aspx?id=12 is valid

http://taeyo.net& is not valid

http://www.taeyo.net% is not valid

http://www.taeyo.net/board@ is valid

http://dayofdays.net is valid

http://blog.dayofdays.net is valid

http://blog.dayofdays.net/article is valid

http://dayofdays.net/article/article.aspx?category=test is valid

http://dayofdays.net/test-url is valid

Instance Elapsed Time : 00:00:00.0206471

http://taeyo.net is valid

http://www.taeyo.net is valid

http://www.taeyo.net/board is valid

http://taeyo.net/board/board.aspx is valid

http://taeyo.net/talk/board.aspx?id=12 is valid

http://taeyo.net& is not valid

http://www.taeyo.net% is not valid

http://www.taeyo.net/board@ is valid

http://dayofdays.net is valid

http://blog.dayofdays.net is valid

http://blog.dayofdays.net/article is valid

http://dayofdays.net/article/article.aspx?category=test is valid

http://dayofdays.net/test-url is valid

Static Elapsed Time :   00:00:00.0101541

계속하려면 아무 키나 누르십시오 . . .

 

결과를 보면 단순한 패턴을 사용할 경우 인스턴스를 생성하는 방식과 정적 메서드를 사용하는 방식 중 인스턴스를 생성하는 방식이 0.0007초 더 빠르지만 그 차이는 미미한 것으로 보이며, 동일한 코드를 복잡한 패턴으로 사용할 경우 거의 0.01초 차이로 정적 메서드를 사용한 방식이 빠른 것을 확인할 수 있습니다.

 

이렇게 성능에서 차이가 나는 것은 복잡한 정규표현식 패턴은 opcode로 변경 되는데 더 많은 비용을 필요로 하지만,정적 메서드 호출은 정규표현식 패턴을 캐시 하기 때문에 그 성능이 향상되는 것입니다. 즉, 인스턴스 메서드의 호출은 매번 정규표현식을 opcode로 변경하는 반면, 정적 메서드 호출은 캐시 된 정규표현식과 다른 패턴이 지정된 경우에만 opcode로 변환하고 캐시에 존재할 경우 캐시에서 opcode를 가져와 사용하게 되는 것입니다.

 

오직 정적 메서드만 캐시가 된다는 것을 기억해 두세요. 캐시의 크기는 Regex.CacheSize 속성을 통해 변경 가능하며 기본 캐시의 크기는 15입니다. 만약 캐시의 크기를 초과하여 사용한다면 최근에 사용되지 않은 캐시를 새로운 정규표현식으로 대체하게 됩니다.

 

다섯 가지 정규표현식 생성 방식 및 성능

지금까지는 인스턴스 메서드와 정적 메서드에 초점을 맞추어 알아보았습니다. 이제 좀 더 성능 향상을 가져올 수 있는 컴파일 된 정규표현식에 대해 알아보도록 하겠습니다.

 

.NET 에서 정규표현식 엔진은 세가지 방식으로 정규표현식을 만들어 낼 수 있습니다.

 

1.      정규표현식 인터프리트 - Regex 개체를 생성하거나 Regex 클래스의 정적 메서드를 호출 할 때(정적 메서드에 지정된 정규표현식이 캐시에서 발견되지 않은 경우) 정규표현식은 opcode로 변환됩니다. 메서드가 호출되면 opcode는 MSIL로 변환되고 JIT 컴파일러에 의해 실행됩니다. 이 방식을 인터프리트 방식이라 하며 시작 시간이 빠른 반면 대신 실행시간이 길어집니다.

2.      정규표현식 컴파일 - Regex 개체를 생성하거나 Regex 클래스의 정적 메서드를 호출 할 때(정적 메서드에 지정된 정규표현식이 캐시에서 발견되지 않은 경우) RegexOptions.Compiled 옵션을 지정하면 정규표현식은 MSIL로 변환됩니다. 메서드가 호출되면 JIT 컴파일러에 의해 MSIL이 실행됩니다. 컴파일 된 정규표현식은 인스턴스 및 정적 메서드 호출에 모두 사용될 수 있습니다. 컴파일 된 정규표현식은 실행시간이 빠른 반면 시작 시간이 길어집니다.

3.      정규표현식을 별도의 어셈블리로 분리 – 여러 개의 정규표현식을 별도의 어셈블리에 별도의 클래스로 생성합니다. 어셈블리로부터 MSIL이 로드 되고 JIT 컴파일러에 의해 실행됩니다. 여러 클래스에서 참조를 통해 사용가능하며 시작 시 드는 비용을 런타임에서 디자인 타임으로 옮긴다는 장점이 있습니다. 즉, 런타임 환경에서 시작시간과 실행시간 모두 빠르다는 장점이 있습니다.

 

세가지 방식은 시작시간과 실행시간의 장단점이 존재합니다. 이제 앞에서 설명한 모든 방식을 사용하여 성능을 측정해 보도록 하겠습니다. 다음 코드는 다섯 가지 방식의 성능 측정을 위한 테스트 코드입니다.

 

다섯 가지 방식

1.     인터프리트 방식의 인스턴스 메서드 사용

2.     컴파일 방식의 인스턴스 메서드 사용

3.     인터프리트 방식의 정적 메서드 사용

4.     컴파일 방식의 정적 메서드 사용

5.     별도의 어셈블리를 참조하여 인스턴스 메서드 사용

 

private static void PerformanceTest(string input)

{

    Stopwatch sw = null;

    string pUrl = @"https?://([\w-]+\.)+[\w-]+(/[\w-./?&%=]*)?";

 

    //[인스턴스 메서드] 인터프리트

    sw = Stopwatch.StartNew();

    Regex regex1 = new Regex(pUrl);

    MatchCollection matches1 = regex1.Matches(input);

    foreach (Match match in matches1)

    {

    }

    sw.Stop();

    Console.WriteLine("[Instance][Interpret] Matches Count={0}, Elapsed Time : {1}", matches1.Count, sw.Elapsed);

 

    //[인스턴스 메서드] 컴파일

    sw = Stopwatch.StartNew();

    Regex regex2 = new Regex(pUrl, RegexOptions.Compiled);

    MatchCollection matches2 = regex1.Matches(input);

    foreach (Match match in matches2)

    {

    }

    sw.Stop();

    Console.WriteLine("[Instance][Compile] Matches Count={0}, Elapsed Time : {1}", matches2.Count, sw.Elapsed);

 

    //[정적 메서드] 인터프리트

    sw = Stopwatch.StartNew();

    MatchCollection matches3 = Regex.Matches(input, pUrl);

    foreach (Match match in matches3)

    {

    }

    sw.Stop();

    Console.WriteLine("[Static][Interpret], Matches Count={0}, Elapsed Time : {1}", matches3.Count, sw.Elapsed);

 

    //[정적 메서드] 컴파일

    sw = Stopwatch.StartNew();

    MatchCollection matches4 = Regex.Matches(input, pUrl, RegexOptions.Compiled);

    foreach (Match match in matches4)

    {

    }

    sw.Stop();

    Console.WriteLine("[Static][Compile] Matches Count={0}, Elapsed Time : {1}", matches4.Count, sw.Elapsed);

 

    //[인스턴스 메서드] 어셈블리

    sw = Stopwatch.StartNew();

    URLRegex urlRegex1 = new URLRegex();

    MatchCollection matches5 = urlRegex1.Matches(input);

    foreach (Match match in matches5)

    {

    }

    sw.Stop();

    Console.WriteLine("[Instance][Assembly] Matches Count={0}, Elapsed Time : {1}", matches5.Count, sw.Elapsed);

 

}

 

다음은 코드를 실행한 결과 입니다.

 

1. 소량 데이터를 사용한 검사---------------

[Instance][Interpret] Matches Count=1, Elapsed Time : 00:00:00.0000213

[Instance][Compile] Matches Count=1, Elapsed Time : 00:00:00.0000086

[Static][Interpret], Matches Count=1, Elapsed Time : 00:00:00.0000139

[Static][Compile] Matches Count=1, Elapsed Time : 00:00:00.0000094

[Instance][Assembly] Matches Count=1, Elapsed Time : 00:00:00.0000094

2. 대량 데이터를 사용한 검사---------------

[Instance][Interpret] Matches Count=1048576, Elapsed Time : 00:00:04.9888389

[Instance][Compile] Matches Count=1048576, Elapsed Time : 00:00:05.6970633

[Static][Interpret], Matches Count=1048576, Elapsed Time : 00:00:04.9711290

[Static][Compile] Matches Count=1048576, Elapsed Time : 00:00:04.1505893

[Instance][Assembly] Matches Count=1048576, Elapsed Time : 00:00:03.9443341

계속하려면 아무 키나 누르십시오 . . .

 

결과는 소량의 데이터를 사용한 경우와 대량의 데이터를 사용한 경우로 나누어지며 다음 그래프를 통해 분석 해 보도록 하겠습니다.

 

 

당연한 결과이나 소량의 데이터를 사용할 경우 상당히 적은 시간이 소요되며 대량의 데이터를 사용할 경우 최소 3초 이상의 시간이 소요되는 것을 확인할 수 있습니다. 먼저 소량의 데이터를 처리하는 경우를 살펴보도록 하겠습니다.

 

 

 

 

소량의 데이터를 사용할 경우 별도의 어셈블리를 사용하여 처리하는 것이 가장 빨랐으며 인터프리트 방식의 인스턴스 메서드를 사용하는 것이 가장 느리게 나타났습니다.

 

 

 

대량의 데이터를 사용할 경우도 별도의 어셈블리를 사용하여 처리하는 것이 가장 빨랐으며 컴파일 방식의 인스턴스 메서드를 사용하는 것이 가장 느리게 나타났습니다.

 

적절한 방식의 선택

결론적으로 별도의 어셈블리를 사용하는 방식이 가장 빠르고 인스턴스를 생성하는 방식이 가장 느렸지만 별도의 어셈블리를 사용하는 경우에는 정적 메서드를 사용하는 것처럼 유연하지는 못합니다. 그래서 닷넷에서 정규표현식의 성능을 최대로 향상시키기 위해서 Regex 를 사용할 때는 다음처럼 하기를 권장합니다.

 

1.     동일한 Regex 개체를 반복적으로 생성하기 보다는 정적 메서드를 사용합니다.

2.     정규표현식 패턴을 사용하는 회수가 적고 패턴이 상대적으로 간단한 경우 인터프리트 방식을 사용합니다.

3.     동일한 정규표현식을 수 많은 곳에서 사용할 경우 성능 최적화를 위해 컴파일 된 정규표현식을 사용합니다.

4.     정규표현식 패턴과 옵션을 변경할 필요가 없으며 최상의 성능을 내기 위해서는 외부 어셈블리로 컴파일 하는 방식을 사용합니다.

 

적절한 방식으로 정규표현식을 사용할 경우 훌륭한 성능으로 패턴 매칭 및 유효성 검사검색 등에 사용할 수 있을 것입니다정규표현식을 통한 문자열 작업은 직접 사용해 보지 않고는 그 강력함을 알 수 없습니다우리 모두 정규표현식을 잘 다루는 개발자가 되어 보자구요^^

 

--



출처 : http://kyeongkyun.tistory.com/91

관련글 더보기