
개요
가끔씩 문자열은 정규 표현식과 일치 시키는데 어려움을 겪을 수도 있습니다. 예를 들어, 정확히 일치하고자 하는것이 무엇인지 알 수 없지만 바로 앞에 오는 것이 무엇인지, 바로 뒤에 무엇이 없는지와 같이 주변 환경을 인식할 수 있습니다. 이러한 경우에는 Lookaround assertions을 사용할 수 있습니다. 이러한 표현식은 항목이 일치하는지 여부만 나타내지만 결과에는 포함되지 않으므로 assertions라고 합니다.
Positive Lookahead
Java 파일을 import를 분석하고 싶다고 가정해 보겠습니다. 먼저 import 키워드 뒤에 “static” 키워드가 오는지 확인하여 static인 import문을 찾아보겠습니다.
주 표현식 import 이후 문자 “static” 그룹을 매치(일치)시키기 위해 표현식에서 (?=criteria) 구문과 함께 Positive Lookagead assertion을 사용하겠습니다.
@Test
void PositiveLookaheadAssertionTest() {
String mainStatement = "import static org.junit.jupiter.api.Assertions.assertEquals;";
Pattern pattern = Pattern.compile("import (?=static).+");
Matcher matcher = pattern.matcher(mainStatement);
assertTrue(matcher.find());
assertEquals(mainStatement, matcher.group());
assertFalse(pattern.matcher("import java.util.regex.Matcher;").find());
}
Negative Lookahead
다음으로, 이전 예제와 정반대로 수행하여 static 없는 import 구문을 찾아보겠습니다. static 키워드가 import 키워드를 따르지 않는지 확인하여 이를 수행해 보겠습니다.
주 표현식 import 뒤에 문자 “static” 그룹이 매치(일치)할 수 없도록 표현식에서 (?!criteria)구문과 함께 Negative Lookahead assertion을 사용하겠습니다.
@Test
void NegativeLookaheadAssertionTest() {
String mainStatement = "import java.util.regex.Matcher;";
Pattern pattern = Pattern.compile("import (?!static).+");
Matcher matcher = pattern.matcher(mainStatement);
assertTrue(matcher.find());
assertEquals(mainStatement, matcher.group());
assertFalse(pattern.matcher("import static org.junit.jupiter.api.Assertions.assertEquals;").find());
}
Java에서 Lookbehind의 제약사항
Java 8까지는 + 및 *와 같은 바인딩되지 않은 수량자가 LookBehind assertion내에서 허용되지 않는다는 제한 사항을 만날 수 있습니다. 즉, 예를 들어 다음 assertion은 Java 8까지 PatternSyntaxException을 발생합니다.
- (?<!fo+)bar, 하나 이상의 “o”문자가 있는 “fo”가 앞에 오면 bar와 일치시키고 싶지 않는다.
- ?<!fo*)bar, 여기서 bar 앞에 “f” 문자가 있고 그 뒤에 0개 이상의 “o”문자가 오면 bar와 일치시키지 않으려고 한다.
- (?<!fo{2,})bar, 두 개 이상의 “o”문자가 있는 foo가 앞에 오면 bar와 일치시키고 싶지 않다.
해결 방법으로 상한이 지정된 중괄호 수량자를 사용할 수 있습니다(예. (?<!fo{2,4})bar). 여기서 “f” 문자 다음의 “o” 문자 수를 4로 최대화 합니다.
Java 9부터 LookBehind에서 바인딩되지 않은 수량자를 사용할 수 있습니다. 그러나 정규식 구현의 메모리 소비로 인해 (?<!fo{2,2000})bar 대신 (?<!fo{2,20})bar와 같이 합리적인 상한이 있는 Lookbehind의 수량자만 사용하는 것이 좋습니다.
Positive Lookbehind
JUnit4와 JUnit5간의 import를 구별하는 것을 분석해보자. 먼저 assertEquals 메소드에 대한 import 문이 jupiter 패키지에서 가져온 것인지 확인해 보겠습니다.
주 표현식 .*assertEquals 앞의 문자 그룹 jupiter를 매치(일치)하기 위해 표현식에서 (?<=criteria)구문과 함께 Positive Lookbehind assertion을 사용하겠습니다.
@Test
void PositiveLookbehindAssertionTest() {
String mainStatement = "import static org.junit.jupiter.api.Assertions.assertEquals;";
Pattern pattern = Pattern.compile(".*(?<=jupiter).*assertEquals;");
Matcher matcher = pattern.matcher(mainStatement);
assertTrue(matcher.find());
assertEquals(mainStatement, matcher.group());
assertFalse(pattern.matcher("import static org.junit.Assert.assertEquals;").find());
}
Negative Lookbehind
다음으로, 이전 예제와 정반대로 수행하여 jupiter 패키지에 속하지 않은 import문을 찾아보겠습니다.
이를 위해서 표현식에서(?<!criteria) 구문과 함꼐 Negative Lookbehind assertion을 사용하여 문자 그룹 jupiter.{0,30}이 주요 표현식 assertEquals 이전에 매치(일치)할 수 없도록 합니다.
@Test
void NegativeLookbehindAssertionTest() {
String mainStatement = "import static org.junit.Assert.assertEquals;";
Pattern pattern = Pattern.compile(".*(?<!jupiter.{0,30})assertEquals;");
Matcher matcher = pattern.matcher(mainStatement);
assertTrue(matcher.find());
assertEquals(mainStatement, matcher.group());
assertFalse(pattern.matcher("import static org.junit.jupiter.api.Assertions.assertEquals;").find());
}
결론
정규식 문자열을 매치(일치)시키는 몇 가지 사례를 해결하기 위해 네 가지 유형의 정규식 lookaround를 사용하는 방법을 살펴보았습니다.
상황에 맞게 잘 쓰도록 해봅시다.
출처: https://www.baeldung.com/java-regex-lookahead-lookbehind#negative-lookbehind