[Java] 정규 표현식(Regular Expression)의 이해

dEpayse
dEpayse_publication
16 min readFeb 21, 2020

정규 표현식을 사용하려고 할 때 마다, 뭔가 복잡하고 정상적으로 작동하지 않을 것 같은 불안감이 있었다. 그래서 다음에 다시 봐도 어렵게 느껴지지 않을만하게 정리를 한 번 해야겠다고 생각했다.

*개인 공부를 위해 작성되었으며, oracle 문서를 기반하여 여러 웹사이트에서 찾은 정보를 정리한 포스트입니다. 부정확한 부분이 있을 수 있으니 양해를 부탁드리며, 틀린 점을 댓글 혹은 메일로 알려주시면 수정하겠습니다.*

이 정규 표현식 포스트는 Java 언어를 기준으로 작성되었으며, 따라서 다른 언어와는 다른 점이 있을 수 있다.

정규 표현식이란?

정규 표현식은 줄여서 정규식이라고도 하며, 영어로는 Regular Expression, 줄여서 regex, regexp라고도 한다. 정규 표현식은 특정한 규칙을 가진 문자열의 집합을 표현하기 위해 쓰이는 형식언어이다.

정규 표현식을 사용하는 이유

그럼 정규 표현식은 어떨 때 사용하면 좋을까?

어떤 문자열에서 특정한 조건의 문자열을 찾고 싶을 때, 그 조건이 복잡하다면 정규 표현식이 도움이 될 수 있다.

하나의 예로 비밀번호의 보안성을 위해 사용자가 비밀번호를 설정할 때, ‘ 최소 8자리에 숫자, 문자, 특수문자 각각 1개 이상 포함’이 라는 조건이 정해져있다면, 우리는 정규 표현식을 사용하지 않을 때와 정규 표현식을 사용할 때 아래와 같은 차이를 확인할 수 있다.

정규표현식 사용 예시

위와 같이 정규 표현식의 사용으로 코드를 간결하게 작성할 수 있는데, 간결한 만큼 문법을 알지 못한다면 이해하기도 매우 힘들다. 정규 표현식은 이 이외에도 전화번호 ,URL 등에도 유용하게 쓰일 수 있다.

Java의 정규 표현식

Java 정규 표현식은 어떤 종류일까

정규 표현식에는 여러 종류가 있지만 가장 대표적인 것은

-UNIX 계열의 표준 정규표현식인 POSIX의 정규 표현식,

-POSIX 정규표현식에서 확장된 Perl방식의 정규표현식이 있다.

Java에서는 Perl의 방식과 유사한 방식을 선택하고 있으나, Perl 방식과 완전히 똑같은 것은 아니고, 그 차이점이 oralcle 문서에 수록되어 있으며, oracle 문서는 google 검색을 이용하거나 본 포스트의 최하단 부분에 링크를 참고하자.

Java에서 regex의 쓰임

java에서 regex를 어떻게 사용하는 지와, 어디에서 사용되는지 알아보자.

1. 내장 함수의 인자로 사용됨

Java의 함수에서, 인자로 String regex라 표기되어 있는 것들이 있는데, 이 녀석들이 정규 표현식을 사용하는 함수들이다. 이 함수들은 java.lang.String 즉, String 객체가 사용할 수 있는 함수들이며, 자주 사용 될 만한 함수들만 기재하였다. (다르게 말하면, 이 함수들은 regex라 표현된 String 인자를 단순 문자열이 아닌 정규 표현식으로 인식한다!)

  • boolean matches(String regex) : 인자로 주어진 정규식과 매치되는 값이 있는지 확인하여 true 혹은 false를 반환. 이 함수를 이용하여 간단하게 정규 표현식을 test할 수 있다.
  • String replaceAll(String regex, String replacement) : 문자열 내에 인자로 주어진 정규식과 매치되는 모든 substring을 replacement로 바꾼다.
  • String[] split(String regex) : 인자로 주어진 정규식과 매치되는 문자열을 구분자로 하여, 구분자를 기준으로 나뉜 문자열들을 문자열의 배열로 반환한다.

2. 정규 표현식을 다루는 class 사용하기

Java에서는 java.util.regex 의 Pattern과 Matcher 클래스를 사용하여 정규 표현식을 다룰 수 있다. 이 클래스들의 객체를 사용하는 가장 기본적인 방법은 다음 예시를 통해 볼 수 있다. 이 방법을 통하여도 정규 표현식을 test할 수 있다.

import java.util.regex.Pattern;
import java.util.regex.Matcher;
Pattern p = Pattern.compile(".at");
Matcher m = p.matcher("cat");
System.out.println(m.matches());

사용하는 것의 장점은, Pattern 객체를 생성하여 compile을 한 번 실행한다면, 그 정규 표현식을 검색하고 싶을 때 다시 컴파일하지 않아도 되기에 속도를 높일 수 있다(재사용이 많을 시에는 더 유용, 단일 사용이라면 Pattern클래스의 matches 함수를 사용하여도 됨). 또한, 이 Pattern과 Matcher는 regex를 다루는 클래스이므로 regex에 관한 다양한 기능을 이용할 수 있으며, 본 포스트에서는 이 모든 것을 다루지 않고 정규 표현식의 사용법과 문법을 위주로 다룬다.

정규 표현식 사용하기(문법)

정규 표현식을 사용하기 전에, 정규 표현식은 특정한 규칙을 표현하기 위해 정규 표현식‘만의’ 문법을 갖고 있다는 사실을 알고 시작하자. 정규 표현식이 어렵고 복잡해보이는 이유이기도 하다. 이제부터 작성되는 정규 표현식 문법은 oracle의 문서를 참고하여 이해하기 쉽게, 그리고 문법에 관하여는 최대한 모든 내용을 정리하려 노력했다. 정리는

  1. 모든 문자를 정규 표현식으로 표현하기
  2. 범위와 집합의 개념을 이용하여 문자열 표현하기
  3. 수량 한정자를 이용하여 지정된 개수만큼 찾기
  4. 그룹 개념 이용하기
  5. 기타 문법 이용하기

순으로 진행하였으며, 내가 생각하기에, 흐름을 따라 이해하기 쉽게 느껴지는 순서임을 밝힌다.

먼저, 모든 문자 각각을 표현하는 방법부터 알아보자.

1. 모든 문자를 정규 표현식으로 표현하기

char 형식의 문자를 정규 표현식으로 표현하기
char 형식의 문자를 정규 표현식으로 표현하기
8진수, 16진수 설명을 위한 하나의 예시
8진수, 16진수 설명을 위한 하나의 예시

위의 표를 보면 char 형식의 어떤 임의의 문자를 표현하는 방법을 알 수 있다. 그러나 여기서 우리가 위에서 언급한 예시(전화번호, URL, 비밀번호 체크; 만약 이러한 형태의 문자열 다룬다고 할 때) 같은 임의의 문자나 숫자와 같은 것에 사용될 수 있는 것은, 1번 뿐이다.

[주의할 점은 자바에서 \(백슬래시, 역슬래시, 원화 기호)가 포함되는 문법을 사용할 때, 위의 표와 같은 의미로 사용되려면 \를 한번 더 써줘야한다. 왜냐하면, 자바의 String 자체에서 \가 escape character로 쓰이기 때문에, 이 String이 regex engine에 쓰이기 위해서는 \를 escape 시켜 regex의 문법으로 쓰일 수 있게 한다.](하단 그림 참조)

그렇다면 어떤 형태가 정규 표현식을 사용하는데 큰 장점이 될까? 상단의 표의 1번에 x는 해당 문자를 써줘야 검색이 가능하다. 예를 들면, 내가 찾고 싶은 단어는 모든 cat과 bat과 rat 이라는 단어인데 이 단어를 모두 찾고 싶다면 모두 써줘야한다는 것이다. 그러나 이 단어들의 공통점, 즉 이 문자열들의 형식은 첫 글자는 관계없이 뒤 두 글자가 at이라는 것이다. 아래의 표를 참고하면, “.at” 이라는 정규 표현식으로 세 단어 모두 검색이 가능하다. 즉, 성격이 비슷한 것들을 모아 간단한 기호로 표현할 수 있는 방법을 정해놓은 것이다. (빨간색 글씨로 쓴 동등한 표현의 사용법은 아직 다루지 않은 내용이므로 당황하지 마세요! 이 포스트에서 다루게 됩니다.) oracle에는 아래 표의 내용이 predefined character classes로 정의되어 있다.

char 형식의 문자를 정규 표현식으로 표현하기2(predefined character classes)
char 형식의 문자를 정규 표현식으로 표현하기2(predefined character classes)

이제 많은 문자열 형식들을 좀 더 간결하게 표현할 수 있다. 그러나 아직 갈 길이 좀 더 남아있다. 예를 들면 ice water 와 hot water 라는 단어를 모두 찾고 싶을 때 또는 student1 부터 student100 까지 전부 찾고 싶을 때 어떻게 하면 될까? 결론부터 말하면 “\\w*water$”, “^student\\d*” 로 찾아낼 수 있다. 벌써부터 뭔가 복잡해 보이지만, 모르는 기호는 *, $, ^ 뿐이니 차근차근 가보자! 먼저 ice water 와 hot water의 예제부터 보면 이 문자열들의 형식은 water로 끝난다는 공통점이 있고, student1~100의 예제 문자열의 공통점은 student로 시작한다는 점이다. 그 공통점이라는 것들이 $,^기호가 의미하는 바인데, 이런 특별한 의미를 가지는 기호들을 메타 문자(metacharacter)라고 한다. predefined character class의 ‘.’ 또한 메타 문자에 속한다.

정규 표현식의 메타 문자(metacharacters)
정규 표현식의 메타 문자(metacharacters)

이제 이 기호들을 사용하여 정규 표현식을 한 층 더 폭 넓게 표현할 수 있는데, 여기서는 메타 문자에는 어떤 것들이 있는지 살펴보는 것이 목적이며, 위 표에서 6번에서 11번까지의 기호들은 더 내용이 많기 때문에 다음 파트들에서 다룬다.

2. 범위와 집합의 개념을 이용하여 문자열 표현하기

상단 메타 문자 표의 6번에서, [ ] 기호를 간단하게 보았다. [ ] 내부에서는 범위와 집합의 개념을 사용하여 어떤 하나의 문자를 표현할 수 있는데, 예를 들면 위의 cat, rat, bat을 찾는 예제에서, 정규 표현식 “.at”을 사용하면 찾고자 하는 단어들을 모두 찾을 수 있지만, 엉뚱하게 ‘ at’과 같은 공백을 포함한 at이나, fat, eat이라는 단어 또한 매치된다. 이럴 때 “[crb]at” 이라는 기호를 사용하면 우리가 원하는 단어만 검색할 수 있다.

주의) [ ] 안에서는 메타 문자 중 \(백슬래시, 역슬래시, 원화 기호)와 | 만 메타 문자와 같은 의미로 사용되며, 나머지 메타 문자는 같은 의미로 사용할 수 없으며 \,| 그리고 ^문자를 제외한 문자는 문자 그대로로 사용될 수 있다. 이유는 [ ]는 결국 하나의 문자를 의미하기 때문이라고 추정된다!

범위와 집합 개념을 이용하여 문자열 표시
범위와 집합 개념을 이용하여 문자열 표시

위의 표를 보고 알 수 있는 것은, [ ] 기호 안에서는 아무 기호 없이 여러 문자를 붙여쓰는 것은 합집합의 개념을, &&는 교집합의 개념을, ^는 여집합의 개념을 사용하고 있고 -는 범위의 개념(~에서 ~까지, 닫힌 구간)을 사용하고 있는 것을 알 수 있다.

3. 수량 한정자를 이용하여 지정된 개수만큼 찾기

수량 한정자 역시 모든 문자를 정규 표현식으로 표현하기의 메타 문자 파트에서 간단하게 살펴보았는데, 다시 한 번 기본을 짚은 후에 조금 더 나아가서 매칭 방법에 대하여 알아볼 예정이다. 매칭 방법에는 greedy, reluctant, possessive 방식이 있으며, 기본 방식은 greedy 이다. 방식에 따라 매칭 결과를 다르게 할 수 있으니 살펴본 후에 넘어가도록 하자.

수량 한정자
수량 한정자

상단의 표를 보고 나면 이제 내가 원하는 문자를 원하는 개수만큼 찾을 수 있다.

수량 한정자 방식 비교
수량 한정자 방식 비교

수량 한정자의 방식에 따른 사용은 위와 같다. 표의 예시를 보면 수량 한정자가 차지하는 양과 방법에 따라 각 방식의 이름을 붙인 것을 유추할 수 있다. 또한 oracle의 문서에는 이렇게 수량 한정자가 차지한 것을 ‘eat’ 이라고 표현하고 있다.

결론적으로는 greedy 방식은 대체로 좀 더 긴 매칭 결과가 나오고, reluctant는 좀 더 짧지만 대체로 더 많은 매칭 결과가 나올 수 있다. possessive 방식은 greedy 방식의 맨 처음 부분의 알고리즘만 수행하는 방식으로, ‘backtracking’ 이 없기 때문에 일치하지 않는 문자열을 찾을 때 greedy 보다 성능이 좋을 수 있다.

4. 그룹 개념 이용하기

java의 정규 표현식를 이용하여 regex에서 group을 이용할 수 있다. group은 사용할 정규 표현식 내에서 그룹을 지을 수 있는 기능이며, 간단한 예를 통해 이해를 해보자.

Example) 그룹의 개념

(위와 같은 시스템을 이용하는 경우는 드물겠지만, group의 개념을 이해하기 위한 예시임을 알린다.) 메뉴가 세 개 뿐인 덮밥집에서, 한 손님이 하나의 메뉴만 주문한다고 가정하자. 사용자는 다음 보기 중 하나를 입력한다면 올바른 입력이라고 할 수 있다.

다음과 같은 정규 표현식으로 사용자가 올바른 입력을 하였는지 검사할 수 있다.

위 정규 표현식에서 첫 번째 그룹이 ‘(가츠|규|부타)’ 이고 두 번째 그룹 이 ‘(동)’으로 나타난다. 여기서 첫 번째 그룹, 두 번째 그룹이라고 표현하였는데, 실제로 \\1, \\2라는 정규 표현식을 통해 상단의 예시 정규 표현식 내에서 사용하면 첫 번째 그룹, 두 번째 그룹 문자열로 인식된다. 그룹의 순서 정규 표현식에서 왼쪽에서 오른쪽 순으로, 왼쪽 괄호 ‘ ( ’의 등장 순서에 따라 정해진다. 이와 같이 캡쳐된 그룹을 재사용하는 것을 그룹 역참조(Back references)라고 한다. 이어지는 예시는 그룹 역참조의 예시이다.

Example) Back references, non-capturing group

만약 두 명 혹은 세 명의 손님이 일행으로 식당을 방문하여, 사람마다 각각 다른 메뉴를 하나씩 주문한다면, 사용자의 입력은 다음 보기 중 하나와 같으면 올바른 입력이라고 할 수 있다. 여기서 사용자는 콤마(,)로 메뉴들을 구분할 수도 있고, 스페이스( )로 메뉴를 구분할 수도 있다고 가정한다.

이 때 우리는 바로 위 그룹의 개념 예제에서 본 정규 표현식를 이용하여 찾을 수는 있지만(Pattern과 Matcher 클래스를 이용하였을 때에 한함), 입력 전체가 올바른지는 알 수가 없다.

만약 다음과 같은 정규 표현식을 사용한다면 입력 전체가 올바른 입력인지 알 수 있다. (이 식의 이해에 대한 설명은 생략한다. 정규 표현식의 메타 문자에 관한 내용은 포스트의 상단에서 찾아볼 수 있다.)

여기서 그룹의 순서를 이용하여 그룹 역참조를 사용한다면, 다음과 같이 사용할 수 있다.

이 정규 표현식의 두 번째 그룹이 ‘(동)’이고, 그 이후에 ‘동’이 반복하여 나올 것이므로, 백슬래쉬와 몇 번째 그룹인지를 사용하면 그룹의 문자열을 역참조하여 해당 정규 표현식에서 사용할 수 있다. 여기서 주의해야 할 점은, 입력된 문장에서, 앞의 ‘(가츠|규|부타)’에 해당하는 첫 번째 그룹에서 가츠가 나왔다면, 첫 번째 그룹은 ‘(가츠|규|부타)’의 표현과 일치하는 것이 아니고 ‘가츠’로 정해진다.

만약 여기서 ‘(가츠|규|부타)’ 그룹을 순서에 포함시키고 싶지 않다면, 즉 캡쳐하고 싶지 않다면, 아래와 같이 정규표현식을 작성하면 된다. (?:regex)와 같은 그룹을 non-capturing group 이라고 한다.

다음과 같이 그룹에 이름을 붙여 사용도 가능하다.

처음 그룹에 이름을 붙일 때는 (?<그룹이름>regex) 와 같은 형태로, 사용할 때에는 \\k<정한 그룹이름> 형태로 사용할 수 있고, 주의할 점은 바로 위의 그룹 순서로 역참조와 같이 그룹이 정규 표현식 자체로 정해지는 것이 아닌 입력된 문자열에서 그 그룹에 해당하는 문자열으로 정해진다는 점이다.

아래는 그룹의 개념을 정리해놓은 표이다.

그룹 개념 이용하기
그룹 개념 이용하기

5. 기타 문법 이용하기

POSIX character classes
POSIX character classes

상단의 표는 US-ASCII 문자만 적용할 수 있는 표현들이다.

oracle문서에는 POSIX character classes로 정의되어 있다.

boundary matchers
boundary matchers

상단의 표는 경계부분을 나타낼 수 있는 표현들이다. 메타 문자인 ^, $도 여기 속하지만, 모든 문자를 정규 표현식으로 표현하기의 메타 문자에 설명되어 있으므로 생략하였다.

flags
flags

상단의 표는 플래그(flag)를 사용하여 정규 표현식의 상세 조건을 설정할 수 있는 방법이다. 진한 노랑으로 표시된 칸은, 칸 내부의 빨간 색 괄호 그룹을 non-capturing group으로 취급하게 한다. 또한, Pattern 클래스를 사용하여 같은 설정을 할 수 있는데, compile할 때 입력 변수로 표 예시에 기재된 플래그를 추가해주면 된다.

--

--

dEpayse
dEpayse_publication

나뿐만 아니라 다른 사람들도 이해할 수 있도록 작성하는, 친절한 블로그를 목표로.