개요
우아한 테크코스 프리코스 1주차에서 문자열 처리를 해야하는 미션이 있다. 일정한 규칙을 가지고 이 문자열이 올바른 문자열인지 판단하는 기능이 필요했다.
처음에는 어떻게든 substring으로 쪼개서 판단해보려 했다. 하지만 판단해야 하는 경우의 수가 많고, 이 경우의 수에 맞춰 코드를 작성할 생각을 하니 머리가 아팠다. 그러다 예전 나를 많이 도와주고, 나를 서버의 길로 들게 해준 형이 정규표현식으로 코드를 짰던 걸 본 게 떠올랐다. 그래서 정규표현식을 공부할 겸 정리해놓고, 나중에 까먹을 때마다 들어와서 보려고 한다.
정규표현식(Regular Expression)이란?
정규표현식 또는 정규식은 특정한 규칙을 가진 문자열의 집합을 표현하는 데 사용되는 형식 언어이다. 정규표현식은 많은 텍스트 편집기와 프로그래밍 언어에서 문자열의 검색과 치환을 위해 지원하고 있다. 컴퓨터 과학의 정규 언어로부터 유래하였으나 구현체에 따라서 정규언어보다 더 넓은 언어를 표현할 수 있는 경우도 있으며, 심지어 정규 표현식 자체의 문법도 여러 가지 존재하고 있다.
여담: 정규 언어
정규 언어, 합리적 언어는 이론 전산학, 형식 언어 이론에서 정규 표현식을 이용하여 표현할 수 있는 형식 언어(특정 법칙에 따라 적절하게 구성된 문자열 집합)이다.
프로그래밍 언어에서의 정규표현식
이제 실제로 프로그래밍을 할 때의 정규표현식을 알아보자. 정규표현식은 문자열 데이터 중에서 원하는 패턴과 일치하는 문자열 부분을 찾아내기 위해 사용하는 것으로, 미리 정의된 기호와 문자를 이용하여 작성한 문자열이다.
아래 정규표현식들의 기호를 보자.(일일히 다 외울 필요 없다. 일단 필요한 부분을 사전 찾듯이 찾고, 직접 작성하여 사용해보자. 여담으로 20년차 개발자도 정규표현식은 다 못외웠다고 한다)
정규표현식 기호 모음
정규 수량 기호
정규표현식 그룹 캡쳐 기호
자주 사용되는 정규표현식 샘플
정규표현식 문법
자바 String의 정규표현식 문법
matches
// matches (특정 패턴과 일치하는지 확인)
String email = "test@example.com";
boolean isValidEmail = email.matches("^[\\w-\\.]+@[\\w-\\.]+\\.[a-z]{2,4}$"); // 이메일 형식인지 확인
System.out.println(isValidEmail); // true
replaceAll
// replaceAll (특정 패턴과 일치하는 부분을 치환)
String sentence = "The price is 100 dollars";
String result = sentence.replaceAll("\\d+", "###"); // 숫자를 모두 ###로 치환
System.out.println(result); // The price is ### dollars
split
// split (특정 패턴을 기준으로 문자열을 나누기)
String fruits = "apple,banana,orange";
String[] fruitArray = fruits.split(","); // 콤마(,)를 기준으로 문자열을 나누기
for (String fruit : fruitArray) {
System.out.println(fruit); // apple, banana, orange 각각 출력
}
regex 패키지 클래스
자바에서는 문자열뿐만 아니라 아예 정규표현식을 전문적으로 다루는 클래스인 java.util.regex 패키지를 제공해준다. 패키지 안의 클래스는 Matcher와 Pattern이 있다.
Java에서의 정규 표현식은 내부 데이터 구조로 컴파일된다. 이 컴파일 과정은 시간이 많이 소요된다. 만약 String.matches(String regex)
메서드를 호출할 때마다 지정된 정규 표현식이 다시 컴파일된다. 이런 경우 Pattern, Matcher를 사용하면 성능이 향상된다.
Pattern Class
이 클래스의 주요 역할은 문자열을 정규표현식 객체로 변환해주는 역할을 한다. 만약 정규표현식 문법에 맞춰 구성하지 않으면 예외가 발생한다.
Pattern 클래스는 일반 클래스처럼 공개된 생성자를 제공하지 않는다. 그래서 정규식 패턴 객체를 생성하려면 compile() 정적 메소드를 호출해야 한다. 이렇게 Pattern 객체로 컴파일된 정규식은 뒤의 Matcher 클래스에서 사용된다.
// 문자열 형태의 정규표현식 문법을 정규식 패턴으로 변환
String patternString = "^[0-9]*$";
Pattern pattern = Pattern.compile(patternString); // Pattern 객체로 컴파일된 정규식은 뒤의 Matcher 클래스에서 사용된다
Matcher Class
Matcher 클래스는 대상 문자열의 패턴을 해석하고 주어진 패턴과 일치하는지 판별하고 반환된 필터링된 결과값들을 지니고 있다. Matcher 클래스 역시 Pattern 클래스와 마찬가지로 공개된 생성자가 없다. Matcher 객체는 Pattern 객체의 matcher() 메소드를 호출해서 얻는다.
정규표현식과 이스케이프(\n
) 문자 처리
우아한 테크코스 프리코스 1주차 미션을 구현하는 와중에, 테스트를 수행하다 이상한 점을 발견했다. 구현중인 기능은 간단히 //
와 \n
사이에 있는 문자를 추출해내는 기능이었다. 예를 들어,
//&\n
저기서 &를 추출해내면 되는 것이었다. 그래서 정규표현식으로 //
와 \n
를 기준으로 분리한 뒤, 배열의 값을 가져오는 방식으로 구현하고 있었는데,, 프로그램에서 인식을 제대로 하지 못하였다. 그리고 테스트코드에서는 실제 입력값을 \\n
으로 입력하고 있었다.
@Test
void 커스텀_구분자_사용() {
assertSimpleTest(() -> {
run("//;\\n1");
assertThat(output()).contains("결과 : 1");
});
}
단순히 \n
는 실제 전에 C 프로그래밍을 했을 때, 출력문 뒤에 줄바꿈으로 띄워주기 위한 것으로만 알고 있었는데, 동작에 이상을 일으킬 줄은 몰랐다. 알아보기 전에 의문점은 두가지였다.
- 실제로 프로그램이 실행된 뒤에 입력할 때에는
\n
이 제대로 들어갔는데, 왜 문자열로 선언하여 넣을 때에는 인식하지 못하지? - 그러면
\\n
으로 해야 인식이 되는거면, 왜 정규표현식에서\\n
으로 하면 패턴 인식이 안되는거지?
원인
원인을 찾아본 결과, 내가 두가지 간과하고 있던 점이 있었다.
자바에서 이스케이프 문자를 처리하는 방식
자바에서 기본적으로 \n
을 출력하면, 문장 줄을 넘겨버린다.
System.out.print("나는 \n가 출력되었으면 좋겠어");
를 출력하면
나는
가 출력되었으면 좋겠어
라고 출력된다.
그러면 인식 자체를 문자 그대로가 아닌, 이스케이프 문자로 인식해버린다는 것인데, \n
를 출력하려면 어떻게 해야될까? 답은 \
를 출력하고 n을 출력하면 된다.
자바에서 \
를 출력하기 위해서는 \\
로 입력하면 된다. 다시말해서,
System.out.print("나는 \\가 출력되었으면 좋겠어");
를 출력하면
나는 \가 출력되었으면 좋겠어
라고 출력된다.
그래서 \n
는 \\n
을 넣게 되면 \
과 n
이 붙어 나오지만 자바에서 이스케이프 문자라고 인식하지 않게 된다.
정규표현식에서 특수문자 처리
정규표현식에서 \n
은
\n The newline (line feed) character ('\u000A')
이다. 정규표현식에서 \n
를 이스케이프 문자로 보고 있다는 것이다.
따라서 자바와 같이, 정규표현식에서 문자 그대로의 \n
를 표현하려면 다른 방식이 필요하다. \n
를 문자 그대로 받기 위해서 어떤 방법이 필요한걸까? 오라클에서 자바의 정규표현식 문서를 보면 아래와 같은 내용이 있다.
그러면 \Q
를 시작으로 \E
까지 표현하게 되면 \n
를 인식하게 된다.
하.지.만
자바에서의 이스케이프 문자처리를 잊으면 안된다. 실제 문자열로 작성하려면 다음과 같이 작성해야 인식된다.
String pattern = "\\Q\\n\\E";
실제 과정을 눈으로 보지는 못했지만, 아마 다음과 같다고 생각된다.
- 문자열에서 각
\
문자 처리 ->\Q\n\E
- 문자 처리된 문자를 기준으로 패턴 파악
알고 있었다면 정말 간단한 문제였지만, 알지 못한다면 오래 걸리는(내가 오래 걸렸다…) 문제였다. 이번 기회에 이스케이프 문자 처리에 대해서 알게 되어서 기쁘다. ( 원인 파악을 위해 구글링하다 나도 모르게 개행문자와 CR, LF에 대해서도 알게 되었다.!)
출처