티스토리 뷰

1. 정규표현식이란?

정규표현식(이하 정규식)은 프로그래밍에서 특정한 규칙(패턴)을 가진 문자열의 집합을 표현하는 데 사용하는 형식 언어이다. 정규표현식은 많은 텍스트 편집기와 프로그래밍 언어에서 문자열의 검색과 치환을 위해 지원하고 있으며, 특히 펄과 Tcl은 자체에 강력한 정규 표현식을 구현하고 있다. - 위키백과

영어로는 Regular Expression이라고 하며, 줄여서 regex 혹은 regexp라고도 한다.

 

정규식이라는 게 특정한 규칙을 가진 것이라고 하는데, 정규식을 보면 오히려 규칙이 없이 아무렇게나 입력된 문자처럼 보인다. 그래서 왠지 어렵게 느껴지고 다가가고 싶지 않게 느껴지는 것 같다.

이메일 검증 정규식, 출처: https://regexland.com/most-common-regular-expressions/

 

2. 정규표현식, 꼭 알아야할까?

개발자라면 정규식을 꼭 알아야한다는 말을 여기 저기서 듣거나 보게 된다. 정규식이 뭐길래 그런걸까? 정규식을 공부하고 보니 정규식을 아냐 모르냐는 문자열을 처리하는 데 있어서 많은 효율성의 차이를 낳는다는걸 깨달았다. 서비스를 개발하면서 정규식을 사용할 확률이 높은 상황은 아마도 아이디나 비밀번호를 검증할 때일 것이다. 아이디로 사용하는 이메일 주소가 유효한 메일 주소인지, 혹은 비밀번호가 규칙에 맞는지 말이다.

 

그런데 알고보면 정규식은 그 외에도 우리 생활과 밀접한 많은 곳에서 사용중이다. 문서웹페이지에서 내가 원하는 단어를 검색할 때 정규식을 사용한다. 워드프로세서, 텍스트 에디터에서도 마찬가지다. 특정 문자를 다른 문자로 바꿀 때에도 정규식을 사용한다. 구글과 같은 검색엔진에서도 정규식을 사용한다. 웹크롤링 기술도 정규식을 사용하고, 개발자들이 사용하는 IDE가 보기 좋게 코드의 색을 구분해줄 수 있는 것도 역시 정규식 덕분이다. 이 모두 정규식이 아니라면 상당히 복잡한 방법을 사용해야하며 이는 매우 어려운 일이 될 것이다.

어쩌면 이런 생각을 할 수 있다. '내가 검색엔진을 개발하는 것도 아니고, 워드프로세서를 개발하는 것도 아닌데 배울 필요가 뭐가 있나?' 하지만 정규식을 알면 Hello world 세계에 새로운 지평이 열린다. 정규식을 알면 그동안 무식하게 처리하던 문자열을 더욱 우아하고 멋진 방법으로 할 수 있다는 것을 깨닫게 될 것이다. (내가 그랬다.)

 

물론 정규식을 사용하는 것이 문자열 처리에서 만능 해결책이 될 수 없다. 정규식도 어떻게 사용하느냐에 따라 성능 차이가 날 수 있고, 잘못 작성된 정규식으로 인해 서버가 마비되는 유명한 일화도 있다. 그럼에도 정규식을 잘 알고 사용하면 많은 문제를 해결할 수 있는 힘을 가지게 되는 것과 다름없다.

3. Hello, Regex!

정규식을 사용한다는 것은 주어진 문자열에서 일종의 패턴을 찾는 것이다. 내가 찾는 패턴을 정규표현식을 통해 정의해서 주어진 문자열이 특정 규칙을 따르는지, 혹은 문자열 안에 내가 찾는 단어가 존재하는지 등을 알아내는 것이다. 정규식을 통해 할 수 있는 일들을 나열해보려고 했는데 너무 많아서 그만뒀다. 음..그러니까, 정말 많은 것들을 할 수 있다. 

 

정규표현식을 알아보기에 앞서, 먼저 아래 링크를 브라우저에 띄워놓자. 실시간으로 정규식을 연습해볼 수 있는 웹사이트다. 정규식을 연습하기엔 너무나 좋은 사이트이지만 자바스크립트에 최적화되어있으니 실제 결과는 각 언어와 약간의 차이가 있을 수 있다는 점을 기억하자.

https://regexr.com/6sr67

 

RegExr: Learn, Build, & Test RegEx

RegExr is an online tool to learn, build, & test Regular Expressions (RegEx / RegExp).

regexr.com

regexr 화면 스크린샷

웹사이트에 들어가면 기본적으로 위 화면이 뜨는데, 1)가장 상단의 Expresion에는 정규식을 입력하는 공간이다. 그리고 2)화면 중앙에는 예문이 있는데 사용자가 수정할 수 있고, 현재 입력된 정규식과 매칭되는 문자열이 하이라이트 처리된다. 그리고 3)가장 하단 공간에는 매칭된 문자열에 관한 추가적인 설명이 나오고 있다. 현재 선택되어있는 Explain 탭에서는그룹을 구분해서 보여주는 점과, 색깔로 표시된 영역을 클릭하면 화면 좌측에 사용된 정규식에 대한 설명을 볼 수 있는 점이 좋다. 이처럼 실시간으로 연습하고 결과를 확인할 수 있어서 공부하는 데 많은 도움이 된다. 

 

Kotlin에서 정규식을 어떻게 사용하는지에 대해서는 뒤에서 설명하기로 하고, 위 웹사이트를 통해 연습해보면서 정규식과 친숙해져보려고 한다. 먼저, 정규식을 사용하려면 메타문자를 알아야 한다. 기본적인 메타문자에 대해서는 배워서 남주는 정규표현식(2) - 메타 문자을 참고하면 되지만, 우선 이 글에서는 예문을 통해 정규표현식의 사용방법을 간단하게 알아보려고 한다.

 

4. 이메일 주소를 검증해보자.

이메일 주소 형식이 맞는지를 검사하는 정규식을 작성해보자. 이메일주소 형식이 어떻게 이뤄졌는지 먼저 한글로 풀어보자. 우리가 정하는 이메일 주소 형식은 아래와 같다.

실제 이메일 주소 문법에는 생각보다 많은 특수문자를 사용할 수 있고 은근히 까다로운 조건들에 맞아야 하지만(위키백과 참고), 여기선 편의상 특수문자는 +, -, _, . 이렇게 4가지만 사용 가능한 것으로 하는 등 간소화했다. 

예) gildong.hong@hwalbindang.net

1) 로컬 영역: 영어 소문자 알파벳, 숫자, 특수문자+-_. 
2) 구분자(골뱅이): @
3) 도메인 이름: 영어 소문자 알파벳, 숫자, 하이픈-, 마침표. , 영어 소문자 알파벳

여기서 도메인 이름은 naver.com이나 gmarket.co.kr 과 같은 형식이 될 수 있다. 즉 도메인 이름에는 마침표가 1개 혹은 2개가 들어갈 수 있다. 

 

4-1. 로컬 영역의 패턴 정의하기

우선 도메인 이름 패턴은 뒤에서 처리하고, 먼저 로컬 영역의 패턴부터 정의해보자. 소문자 알파벳과 숫자, 특수문자 4개를 사용할 수 있는 패턴을 정규식으로 표현하면 아래와 같다. 

^[a-z0-9+-_.]+

Chatacter set을 뜻하는 대괄호 [ ] 안에 사용할 수 있는 문자를 정의하면 된다. 대괄호 안에 정의된 문자는 그 문자들 중 어떤 것이든 매칭한다는 의미이다. 범위를 뜻하는 하이픈 - 기호를 사용해서 알파벳과 숫자의 범위를 정의한다. [a-z]는 소문자 알파벳 a부터 z까지를 의미한다. 그리고 사용 가능한 특수문자를 나열했다. 가장 왼쪽에 있는 삿갓 ^ 기호는 찾는 문자열이 문장의 시작 위치에 있어야함을 뜻한다. 문자열 중간에 있으면 찾지 않는다. 그리고 가장 오른쪽에 더하기 + 기호는 대괄호 안에 있는 패턴이 1개 이상 있어야 한다는 의미이다. 더하기 기호가 없으면 정규식은 대괄호 안에 정의된 패턴과 일치하는 1개의 문자만 찾는다. 그러나 이메일 주소는 1개 이상의 문자로 이뤄졌기 때문에 더하기 기호를 사용해서 보다 긴 문자열을 찾아내야 한다.

* 삿갓 ^ 기호를 대괄호 안에서 사용하면 부정(negative)를 뜻한다.

 

regexr 웹사이트 상단 정규식 입력란에 아래처럼 입력해보자. 골뱅이 오른쪽의 마침표는 진짜 마침표가 아니라 메타문자로, 모든 문자를 의미한다.즉 골뱅이 다음에는 어떤 문자든 하나 이상 존재하면 패턴에 매칭되도록 임시적으로 정의한 것이다.

* 대괄호 밖에서 메타문자가 아닌 실제 마침표를 사용하려면 escape 메타문자인 \ 역슬래시 기호와 함께 사용해야 한다. 다른 특수문자도 마찬가지다. 예) \. 

^[a-z0-9+-_.]+@.+

그럼 본문에 아래처럼 우리가 정의한 패턴에 매칭하는 것들만 하이라이트되는 것을 확인할 수 있다. 두 번째 문자열은 정의되지 않은 특수문자인 별표 * 기호가 들어갔기 때문에 매칭되지 않았고, 마지막은 골뱅이 @ 기호 앞에 아무런 문자도 없기 때문에 우리 패턴에 매칭되지 않는다. 그럼 이제 골뱅이 우측의 도메인 이름의 패턴을 정의해보자.

 

4-2. 도메인 이름 영역의 패턴 정의하기

도메인 이름은 알파벳이나 숫자, 그리고 하이픈(-)으로 이루어질 수 있다. 그리고 최소한 마침표가 1개에서 최대 2개가 들어가야 한다. 없어도 안되고 3개 이상이어도 안되며, 마침표는 두 개가 붙어있어도 안된다. 최소 길이와 최대 길이도 정해야하지만 여기서는 생략하자.

 

그럼 일단 알파벳 소문자와 숫자, 그리고 하이픈이 최소 한 개 이상 들어가는 형식을 정해보자. 나는 아래처럼 만들었다. 도메인 패턴에 이걸 적용하면 어떻게 될까?

[a-z0-9-]+

어라? 이번에는 마침표 부터 그 이후의 문자열은 매칭되지 않았다. 왜 그런걸까? 마침표는 a-z에 포함되지 않기 때문이다. 당연한 결과다. 그럼 이번에는 마침표를 패턴에 적용해보자. 마침표는 최소 1개는 반드시 있어야 하니 아래처럼 해보자.

 

[a-z0-9-]+\.[a-z]+

기존에 작성한 패턴 오른쪽에 마침표가 추가됐다. 메타문자가 아닌 실제 마침표가 필요하니 역슬래시 \ 기호와 함께 작성해준다. 그리고 마침표 이후에는 영어 소문자 알파벳이 1개 이상 있어야 하는 패턴이다. 위 패턴을 적용해보자.

이번엔 마침표가 하나뿐인 첫 번째 문자열은 전부 매칭이 되었다. 그러나 .co.kr 형태의 세 번째 문자열은 .kr이 매칭되지 않았다. 이번에는 두번째 문자열까지 모두 매칭되도록 수정해보자. 

 

.co.kr의 경우처럼 마침표가 두 개 들어가는 경우는 가장 뒤의 마침표로 구분되는 레이블이 있을 수도 있고 없을 수도 있는 패턴이 되어야 한다. 이럴 때 사용하는 메타문자가 있다. 바로 ? 물음표 기호이다. 물음표 기호를 붙여서 아래처럼 입력해보자.

^[a-z0-9+-_.]+@[[a-z0-9-]+\.[a-z]+\.[a-z]+?

4-3. 물음표 ? 기호의 기능 (optional & lazy)

어라? 결과가 이상하다. 이번엔 잘 매칭되던 첫 번째 문자열은 매칭되지 않고 세 번째 문자열은 마지막 r이 빠진채로 매칭되었다. 왜 그럴까? 예상대로 당연히 물음표 ? 기호 때문이다. 물음표 기호를 잘못 사용한건데, 물음표 기호는 수량자와 함께 사용되면 lazy 속성을 띄게 된다. 여기서 수량자는 더하기 + 기호다. 여기서는 [a-z]가 최소 1개 이상 있어야 한다는 의미이다. 정규식은 기본적으로 탐욕적(greedy)이기 때문에 물음표 기호와 같은 옵션을 두지 않으면 기본적으로 가능한 많은 케이스를 찾아서 매칭시킨다. 그러나 물음표 기호를 추가하면 게을러(lazy)져서 가능한 적은 케이스만을 찾아서 매칭시켜 주는 것이다. 즉 여기서 물음표 기호는 정규식 \.[a-z]+ 전체를 적용하고 있는게 아니라 더하기 기호에만 적용된 상태이다. 

 

4-4. 그룹(group)으로 묶자

그렇다면 \.[a-z]+ 전체에 물음표 기호를 적용시켜주려면 어떻게 해야 할까? 그룹을 사용해야 한다. 그룹은 소괄호 ( )를 사용한다. 아래처럼 수정해보자. 소괄호로 그룹을 정의한 다음 물음표를 사용했다. 즉 마침표 후에 알파벳 소문자가 1개 이상 있어야 한다는 조건이 있거나 없는 패턴이다. 

(\.[a-z]+)?

아래는 위 패턴을 적용한 전체 정규식이다. regexr에서 적용해보자. 

^[a-z0-9+-_.]+@[[a-z0-9-]+\.[a-z]+(\.[a-z]+)?

성공이다. 여기서 한 가지만 더 알아보자. 아래와 같이 이메일 주소가 문장 중간에 있는 경우 메일 주소를 찾고 싶다면 어떻게 하면 될까? 단순하다. 정규식 가장 앞에 있는 삿갓 ^ 기호를 제거하면 된다. 앞에서 밝혔듯 삿갓 기호는 문장의 시작 위치를 매칭하는 메타 문자다. 

이메일 주소 검증 패턴을 만들어 보았다. 여기서 만들어본 패턴은 정규식 찍먹을 위한 토이 수준의 패턴이지 실제 사용가능한 패턴으로 보기엔 문제가 많다. 실제 적용 가능한 패턴은 아래 처럼 조금 더 복잡한 형태를 띈다.

아래 정규식도 정답이라는 할 수 없다. 각 서비스 회사마다 적용하는 검증 패턴이 조금씩 차이가 있을 수 있다.

이메일 검증 정규식, 출처: https://regexland.com/most-common-regular-expressions/

5. 역참조, Backreference

연속하는 문자를 찾고 싶은 경우에는 어떻게 해야할까? 예를 들어, ss, vv, oo와 같이 두 번 연속하는 문자를 찾고 싶거나 혹은 haha, byebye와 같이 연속하는 단어를 찾고 싶을 때 사용할 수 있는 정규식이 있을까? 당연히 있다! 연속된 문자열을 찾는 메타 문자가 따로 있지는 않지만 방법이 있다. 역참조를 사용하는 것이다. 역참조는 정규식의 그룹을 사용해서 찾은 문자를 정규식 안에서 참조하는 방법이다. 

 

역참조는 역슬래시 / 기호와 참조하려는 그룹의 순서 n을 조합해서 사용한다. 예를 들어 a-z 사이의 알파벳이 두 번 연속 반복된 문자를 찾고 싶다면 아래의 정규식을 사용하면 된다. 

([a-z])\1

캡쳐그룹과 역참조 기호 \1의 조합을 주목하자. 여기서 1이 의미하는 것은 정규식에 정의된 1번째 그룹의 결과값을 참조하겠다는 것이다. 위처럼 정규식을 입력하면 아래와 같은 결과를 얻을 수 있다. 

만약 단어가 연속 두 번 반복하는 패턴을 찾고 싶다면? 단순히 수량자 +를 대괄호 오른쪽에 추가해주면 된다. 

 

역참조는 이렇게 단어의 반복을 찾을 때 유용하게 사용할 수 있다. 하지만 역참조의 본래 목적은 앞에서 이미 찾아낸 검색 결과를 참조해서 정규식에 포함시키고 싶을 때 사용한다. 대표적인 예로 HTML 코드에서 태그가 열릴 때부터 닫힐 때까지의 문자열을 찾고자 할 때 상당히 유용하게 사용할 수 있다. 아래 정규식을 적용해보자. H태그의 시작과 끝을 매칭하는 정규식이다.

<([hH][1-6])>.*?<\/\1>

아래와 같이 H1 태그를 찾아낸 것을 확인할 수 있다.

 

이 글에서는 모든 메타 문자를 다루지는 못했다. 모두 예제를 만들어서 짚고 넘어가고 싶은 욕심은 있으나 시간과 체력과 스크롤 압박의 문제로 생략하기로 하고, 나머지 메타 문자는 이 다음 글 배워서 남주는 정규표현식(2) - 메타 문자 에서 확인해주시길 부탁드린다.

 


☝🏻 One more thing!  Regex in Kotlin

코틀린에서 정규식 클래스를 사용하는 방법은 GM.Lim님의 <Kotlin에서 정규표현식 사용하기> 글을 참고했으니 보다 자세한 내용은 원문을 참고 바란다.

1. 코틀린에서 Regex 객체 생성 방법

Regex 객체를 생성하는 방법은 아래와 같이 3가지가 있다. 

val regex1 = Regex("\\([A-Z])\\w+\\")
val regex2 = "\\(([A-Z])\\w+\\)".toRegex()
val regex3 = """([A-Z])\w+""".toRegex()

위 3가지 방법 모두 동일한 정규식을 생성한다. 그러나 자세히 보면 regex3의 정규식이 나머지 둘과 조금 다른 것을 알 수 있는데, 이스케이프 문자가 생략된 형태이다. 코틀린에서 역슬래시 \ 기호는 이스케이프 문자이므로 직접 사용할 수 없고 반드시 앞에 역슬래시를 추가해서 해당 역슬래시가 일반 문자라는 것을 알려줘야 한다. 그러나 삼중 따옴표(Raw String)를 사용하면 이런 처리 없이 역슬래시를 사용할 수 있기 때문에 보다 깔끔하고 읽기 쉬운 정규표현식을 작성할 수 있다. 나는 개인적으로 이 방식을 선호한다. 

 

입력값이 정규식 패턴과 일치하는지 확인하는 방법들을 하나씩 간략히 알아보자.

1-1. 입력값이 정규식 패턴과 정확히 매칭하는 지 확인할 때, matchEntire( )

val result: MatchResult? = regex.matchEntire("aeiou")

1-2. 입력값에서 정규식 패턴에 매칭하는 첫 번째 문자열을 찾을 때, find( )

val result: MatchResult? = regex.find("aeiou")

matchEntire()와 find() 함수는 MatchResult 객체를 반환하는데, 매칭하는 결과가 없다면 null을 반환한다. 

MatchResult

 

1-3. 입력값에서 정규식 패턴에 매칭하는 모든 문자열을 찾고 싶을 때, findAll( )

val result: Sequence<MatchResult> = regex.findAll("aeiou")

findAll() 함수는 Sequence<MatchResult> 객체를 반환하는데, 매칭하는 결과가 없다면 시퀀스의 길이는 0이다.

 

1-4. 입력값에 정규식 패턴에 매칭하는 문자열이 있는지 알고 싶을 때, containsMatchIn( )

val result: Boolean = regex.containsMatchIn("aeiou")

containsMatchIn( ) 함수는 단순히 입력값에 정규식에 맞는 문자열이 포함되어있으면 true, 그렇지 않으면 false를 반환한다.

 

2. MatchResult 클래스

위에서 알아본 함수 중 containsMatchIn()을 제외한 나머지는 MatchResult 클래스의 객체를 반환하는데, 이 클래스는 정규식의 결과값을 담고 있다. 

 

2-1. 결과 확인하기

MatchResult 클래스의 value 프로퍼티는 입력값에서 정규식 패턴에 매칭하는 결과값을 문자열로 담겨있다. 위에서 실행해본 역참조를 통해 두 번 반복된 문자열을 찾아보는 코드를 작성해보자.

fun main() {
    val regex = """([a-z])\1""".toRegex()
    val results = regex.findAll("http google ssangyong danggeun")

    results.forEach {
        println(it.value)
    }
}

실행 결과:

tt
oo
ss
gg

findAll() 함수로 찾은 Sequence<MatchResult> 객체를 foreEach 문을 실행해 하나식 value를 출력한 결과다. regexr에서 찾은 것과 같은 결과를 얻을 수 있다.

 

위 처럼 단순히 결과에 해당하는 문자열이 아니라, 문자열의 위치를 알고 싶다면? 가능하다. 문자열의 시작값과 끝값을 IntRange 객체로 반환받을 수 있다. value를 range로 바꿔서 실행해보면 아래와 같은 결과를 얻을 수 있다.

1..2
6..7
12..13
25..26

2-2. 그룹 확인하기

방금 사용한 정규식 ([a-z])\1 의 결과는 두 개의 그룹으로 나뉜다. 하나는 1)전체 정규식의 결과이고 다른 하나는 메타 문자 소괄호( ) 안의 2)그룹 정규식의 결과이다. 앞에서 밝혔듯이 역참조를 사용하면 먼저 정의된 그룹에서 찾아낸 결과를 참조할 수 있다고 했다. 즉 여기서 이야기하는 그룹 정규식의 결과란 역참조된 결과값이다. 코드로 확인해보자. 

fun main() {
    val regex = """([a-z])\1""".toRegex()
    val results = regex.findAll("http google ssangyong danggeun")

    results.forEach {
        println(it.groupValues.joinToString(", "))
    }
}

기존 코드에서 println() 안의 코드만 변경했다. groupValues 프로퍼티는 List<String> 객체를 반환하는데, 이 안에 방금 언급한 두 개의 그룹 결과 value가 담기게 된다. 지금 사용하는 정규식의 구조는 두 개의 그룹이 담기는 구조이지만, 정규식이 바뀌면 더 많은 그룹이 담길 수도 있고, 아예 담기지 않을 수도 있다. 글로만 설명해서 이해가 쉽지 않을 수 있다. 빨리 위 코드의 출력 결과를 확인하자.

tt, t
oo, o
ss, s
gg, g

 

findAll( ) 함수의 결과로 Sequence<MatchResult> 안에 총 4개의 아이템이 담겼는데, 각각의 MatchResult 안에 총 2개의 매칭 그룹이 담겨있다. 첫 번째 MatchResult의 groupValues에 담긴 (tt, t)를 보자. 이 값들은 문자열 "http" 안에서 찾아낸 결과값들이다. tt는 전체 정규식의 결과값이고, t는 정규식의 첫 그룹 정규식 ([a-z])의 결과다. 즉 첫 그룹 정규식의 결과 t를 참조해서 이어 붙인 tt를 찾아낸 것이 전체 정규식의 결과인 것이다. 

이처럼 정규식에 그룹이 있다면 그 그룹의 매칭 결과를 따로 확인할 수 있다. 기억할 것은 전체 정규식의 결과가 첫 번째 그룹으로 인식된다는 것이다. 

 

참고한 자료들

- 정규표현식이 사용 사례: https://blog.finxter.com/what-are-regular-expressions-used-for-10-applications/

- 가장 흔히 사용하는 정규표현식 20가지: https://regexland.com/most-common-regular-expressions/

- Kotlin 정규표현식: https://codechacha.com/ko/kotlin-how-to-use-regex/

- Kotlin에서 정규 표현식 사용하기: 링크

댓글