[ 정규표현식 시리즈 ]
몰라서 못 썼던 정규 표현식을 알아보자 ③
# 정규 표현식에서의 "일치"
- 완전 일치 : 정규 표현식이 주어진 문자열 전체에 일치
- 전방 일치 : 정규 표현식이 주어진 문자열의 접두사에 일치
- 후방 일치 : 정규 표현식이 주어진 문자열의 접미사에 일치
- 부분 일치 : 정규 표현식이 주어진 문자열의 부분 문자열에 일치
예를 들어 [ab]*a[ab]{2} 라는 정규 표현식이 있을 때 'aab' 나 'bababa' 는 완전 일치하지만, 'babab' 에는 완전 일치하지 않습니다. 하지만 부분 일치로 봤을 때 babab 또한 일치함을 알 수 있습니다. b 'aba' b 처럼 작은 따옴표 안은 위 정규 표현식에 일치하기에 부분 일치한다고 볼 수 있습니다.
완전 일치는 주로 문자열 검증에 사용됩니다. 백엔드 서버를 만들면서, 특정한 형태의 URL을 처리한다면 완전일치가 적절하지만 문자열 전체를 뒤지면서 검색을 하거나 치환을 한다면 부분 일치가 적절할겁니다.
# 앵커를 활용해서 여러 일치 종류 표현하기
앵커를 활용하여, 행 처음에 ^ 와 행 끝을 나타내는 $ 을 사용함으로써 부분 일치가 기본인 상황에서 다른 일치 종류를 구현해 낼 수 있습니다.
일치 종류 | 정규 표현식 |
부분 일치 | regex |
완전 일치 | ^regex$ |
전방 일치 | ^regex |
후방 일치 | regex$ |
비슷한 방법으로 완전 일치가 기본인 상황에서 사용하는 방법이 있습니다. 이 때는 앵커가 아닌 .* 을 활용해서 작성합니다.
일치 종류 | 정규 표현식 |
완전 일치 | regex |
부분 일치 | .*regex.* |
전방 일치 | regex.* |
후방 일치 | .*regex |
# 서브 매치와 캡처
괄호는 특정한 식이 우선적으로 계산될 수 있도록 하기도 하지만 캡처라는 기능을 가지고 있기도 합니다.
둥근 괄호로 모아진 정규 표현식의 일부분을 서브 패턴(subpattern)이라고 합니다. 만약 주어진 문자열이 정규 표현식 전체에 일치한다면, 개별 서브 패턴에는 부분 문자열이 일치할 것인데, 서브 패턴에 일치한 부분 문자열을 서브 매치(submatch)라 합니다.
예를 들어, (\d+): (\w+) prime \. 라는 정규 표현식에는 (\d+) 와 (\w+) 라는 두개의 서브 패턴이 포함되어 있습니다. 이 정규 표현식에 대해 '57: Grohendiexk prime.' 이라는 문자열은 완전 일치하며 (\d+) 에는 57이, (\w+) 에는 Grothendieck 라는 문자열이 서브 패턴에 대응한다고 할 수 있습니다.
정규 표현식에는 이런 서브 매치를 추출해 낼 방법을 제공하며 이를 캡처라 합니다. 이 서브 패턴에 대해서 서브 매치를 취득하려면 '이 서브 패턴의 서브 매치를 원한다'라고 알려줘야하며 지정 방법으로는 두 가지가 있습니다.
- 순서대로 지정하기
- 이름으로 지정하기
순서대로 지정하기
서브 패턴은 기본적으로 순서대로 지정됩니다. 예를 들어, (\d{2}) / (\d{2}) / (d{4}) 라는 정규 표현식은 왼쪽부터 차례대로 1, 2, 3 이라는 번호가 붙습니다.
만약 괄호가 중첩되어 있다면? ((\d)\d) 라는 예시를 보면 1번 서브 매치는 표현식 전체를 감싸고 있는 괄호이고 2번 서브 매치는 괄호 안의 괄호가 됩니다. 왼쪽부터 번호를 붙인다는 원칙 그대로입니다.
이름으로 지정하기
순서대로 지정하는 경우는 단순하고 빠른 방법이라는 장점이 있지만 알아보기 어렵고 수정에 민감합니다. 이를 해결하고자 등장한 것이 이름 지정 캡처(named capture) 입니다. 이름 지정 캡처는 서브 패턴에 원하는 이름을 붙여서 그 이름을 사용해 서브 매치를 취득하는 것입니다.
사용 방법으로는 (?<name>) 으로 그룹의 이름을 지정하며, 가져다 사용할 때는 자바에서는 matcher.group(name) 으로 사용 가능합니다.
private static final Pattern customPattern = Pattern.compile("(?<day>\d{2})\/(?<month>\d{2})\/(?<year>\d{4})");
public String trans() {
Matcher matcher = customPattern.matcher("21/10/2024");
if (matcher.find()) {
String day = matcher.group("day");
String month = matcher.group("month");
String year = matcher.group("year");
return String.format("%s년 %s월 %s일", year, month, day);
}
return input;
}
}
정규 표현식을 보면 눈에 확 들어오고 이해도 쉽게 되는 것을 알 수 있습니다. 이러면 코드 유지보수에 있어서 좋은 접근 방법이라 생각이 들었습니다.
# 캡처 없이 그룹화
필요없는 부분을 캡처해서 사용하지 않고 있으면 성능이 떨어지는 결과가 나타날텐데 이를 위해 제외시키고자 하는 부분은 (?:) 라는 구문을 사용할 수 있습니다.
private static final Pattern customPattern = Pattern.compile("(?:\d{2})\/(?:\d{2})\/(?:\d{4})");
# 우선 순위는 왼쪽부터 오른쪽
정규 표현식 일치는 기본적으로 왼쪽부터 오른쪽으로 처리됩니다. (a*)([ab]*) 라는 정규 표현식에 'aaa' 를 일치 시켰을 때, ([ab]*) 가 아닌 (a*) 에 매치가 할당되어 matcher.group(1) 에 서브 매치가 구성됩니다.
만약 (fuga | fugah) 라는 정규 표현식이 있고 대응시킬 문자열이 'fugah' 라면 일반적으로 'fugah' 라고 생각하겠지만 이는 완전 일치의 경우에만 입니다. 부분 일치의 경우는 'fuga' 와 대응되는 것이죠.
일반적으로 생각하는 'fugah' 와 대응시키려면 ^(fuga | fugah)$ 로 식을 바꿔줘야합니다. 심화 예시로는 'fugahoge' 라는 문자열이 있을 때 (fuga | fugah)(oge | hoge) 정규 표현식으로 매칭시킨다면 'fuga' 가 먼저 인식되기에 오른쪽은 'hoge' 가 일치됩니다.
참고자료
'시리즈 > 자바' 카테고리의 다른 글
[TEST] 테스트를 위한 JUnit 5 와 AssertJ ① (1) | 2024.10.24 |
---|---|
[JAVA] 몰라서 못 썼던 정규 표현식을 알아보자 ④ (1) | 2024.10.22 |
[JAVA] 몰라서 못 썼던 정규 표현식을 알아보자 ② (1) | 2024.10.20 |
[JAVA] 몰라서 못 썼던 정규 표현식을 알아보자 ① (5) | 2024.10.19 |
[JAVA] 직관성있는 자바 네이밍 규칙 (1) | 2024.10.18 |