개요
이번 예제에서는 정규식을 사요하여 텍스트를 바꾸기 위해 String 클래스에서 제공되는 replacementAll()을 사용하는 방법에 대해서 알아보겠습니다. 또한 동일한 작업을 수행하는 back reference와 lookaround를 배워보고 성능을 비교해 보겠습니다.
replaceAll()과 함께 Back Reference(역참조) 활용
Back Reference(역참조)를 이해하려면 먼저 그룹 일치(matching groups)에 대해서 알아야 합니다. 간단히 말해서 그룹(group)은 단일 단위로 보이는 여러 문자에 지나지 않습니다. 따라서 Back Reference (역참조)는 동일한 정규식 내에서 이전에 일치한 그룹을 다시 참조할 수 있도록 해주는 정규식의 기능입니다. 일반적으로, \1, \2 등과 같이 패턴에서 캡처 그룹을 나타내는 숫자로 표시합니다.
예를 들어, 정규식 (a)(b)\1 은 \1 을 사용하여 캡처된 첫 번째 그룹(이 경우에는 (a))을 다시 참조 합니다.
문자열 교체 작업에서는 이러한 참조를 사용하여 일치하는 텍스트를 원하는 텍스트로 변경합니다. replacementAll()메서드를 사용할 때 대체 문자 열의 갭처 그룹을 $1, $2 등으로 참조합니다.
이제 더 잘 이해하기 위해 다음 사용 사례를 생각해 보겠습니다. 문자열 내의 모든 별표 기호를 제거하고 싶습니다. 따라서 별표가 문자열의 시작이나 끝에 나타나는 경우에만 별표를 유지하고 다른 모든 별표는 제거하는 것이 임무입니다. 예를 들어 *text*는 변경되지 않은 채로 유지되지만 **te*x**t**은 *text*가 됩니다.
Back Reference(역참조) 구현
작업을 완료하기 위해 정규시과 함께 replacementAll()메서드를 사용하고 다음에서 Back Reference(역참조)를 사용합니다.
@Test
void replaceByBackReference() {
String str = "**te*xt**";
String replaced = str.replaceAll("(^\\\\*)|(\\\\*$)|\\\\*", "$1$2");
assertEquals("*text*", replaced);
}
위에서는 세 부분으로 구성된 정규식 “(^\\*)|(\\*$)|\\*”을 정의하고 있습니다. 첫 번쨰 그룹(^\\*) 은 문자열 시작 부분의 별표를 캡처합니다. 두 번째 그룹(\\*$)은 문자열 끝에 있는 별표를 캡처합니다. 세 번째 그룹 \\*은 나머지 별표를 모두 캡처합니다. 따라서 정규식은 문자열의 특정 부분만 선택합니다. 그리고 선택한 부품만 교체됩니다. 다양한 색상으로 다양한 부분을 강조 표시합니다.
즉, 대체 문자열 $1$2는 해당 그룹에서 선택한 모든 문자를 반환하므로 최종 문자열에 유지됩니다.
동일한 작업을 해결하기 위한 다른 접근 방식을 알아보겠습니다.
replaceAll()과 함께 Lookaround 활용
Back Reference(역참조)에 대한 또 다른 접근 방식은 Lokaround를 사용하는 것입니다. 이를 통해 정규식에서 일치(match)하는 항목을 찾을 때 주변 문자를 무시할 수 있습니다. 이 예에서는 보다 직관적인 방법으로 문자열 내의 별표를 제거할 수 있습니다.
@Test
void replaceByLookaround() {
String str = "**te*xt**";
String replacedUsingLookaround = str.replaceAll("(?<!^)\\\\*+(?!$)", "");
assertEquals("*text*", replacedUsingLookaround);
}
이 예에서 (?<!^)\\+는 앞에 문자열의 시작이 없는((?<!^)) 하나 이상의 별표(\\+)를 캡처합니다. 한마디로 Nagative Look Behind를 하는 것입니다. 다음으로 (?!$) 부분은 문자열 끝 뒤에 오는 별표를 무시하도록 정의하는 Negative Lookahead입니다. 마지막으로 여기서 빈 대체 문자열은 일치하는 모든 문자를 제거합니다. 따라서 제거하려는 모든 문자를 선택하므로 이 방법을 추론하기가 더 쉽습니다.
가독성 외에도 이 두 가지 방법은 성능이 다릅니다.
Lookaround vs BackReference 성능비교
이 두 가지 방법의 성능을 비교하기 위해 JMH 라이브러리를 사용하여 많은 수의 문자열 교체를 처리하는 데 각 방법에 필요한 평균 실행 시간을 벤치마킹하고 측정합니다.
성능 테스트에서는 이전 작업과 동일한 별표 예를 사용합니다. 즉, 두 정규식 메서드와 함께 replacementAll() 함수를 1000번 반복적으로 사용하겠습니다.
이 테스트에서는 준비 반복 2회와 측정 반복 5회를 구성합니다. 또한 작업을 완료하는 데 걸리는 평균 시간을 측정합니다.
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import java.util.concurrent.TimeUnit;
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Fork(1)
@Warmup(iterations = 2)
@Measurement(iterations = 5)
public class RegexReplaceStringByBackReferenceAndLookaround {
private static final int ITERATIONS_COUNT = 1000;
@State(Scope.Benchmark)
public static class BenchmarkState {
String testString = "*example*text**with*many*asterisks**".repeat(ITERATIONS_COUNT);
}
@Benchmark
public void backReference(BenchmarkState state) {
state.testString.replaceAll("(^\\\\*)|(\\\\*$)|\\\\*", "$1$2");
}
@Benchmark
public void lookaround(BenchmarkState state) {
state.testString.replaceAll("(?<!^)\\\\*+(?!$)", "");
}
public static void main(String[] args) throws Exception {
Options opt = new OptionsBuilder().include(RegexReplaceStringByBackReferenceAndLookaround.class.getSimpleName())
.build();
new Runner(opt).run();
}
}
이 예제의 결과 출력은 둘러보기 방법이 더 성능이 좋다는 것을 명확하게 나타냅니다.
// lenovo thinkpad-x 12세대 에서 테스트
Benchmark Mode Cnt Score Error Units
RegexReplaceStringByBackReferenceAndLookaround.backReference avgt 5 1.427 ± 0.127 ms/op
RegexReplaceStringByBackReferenceAndLookaround.lookaround avgt 5 0.922 ± 0.302 ms/op
따라서 그룹을 개별적으로 캡처한 다음 해당 그룹을 대체 문자열로 바꾸는 데 오버헤드(overhead)가 필요하기 때문에 Back Reference(역참조)가 느려집니다. 이전에 설명한 대로 둘러보는 동안 문자를 직접 선택하고 제거합니다.
결론
정규식에서 Back Reference(역참조) 및 Lookaround와 함께 replacementAll() 메서드를 사용하는 방법을 살펴보았습니다. Back Reference(역참조)는 일치하는 문자열의 일부를 재사용하는 데 유용하지만 캡처 그룹의 오버헤드로 인해 속도가 느려질 수 있습니다.
참고(진행 시 이슈였던 부분)
- jmh의 benchmark 실행시 다음과 같은 에러발생시 임의로 “resorces/META-INF/” 경로에 “BenchmarkList” 파일을 생성합니다. (내용은 없어도 무관, 빈파일 생성)
Exception in thread "main" java.lang.RuntimeException: ERROR: Unable to find the resource: /META-INF/BenchmarkList
at org.openjdk.jmh.runner.AbstractResourceReader.getReaders(AbstractResourceReader.java:98)
at org.openjdk.jmh.runner.BenchmarkList.find(BenchmarkList.java:124)
at org.openjdk.jmh.runner.Runner.internalRun(Runner.java:252)
at org.openjdk.jmh.runner.Runner.run(Runner.java:208)
at com.liooos.example.other.regex.RegexReplaceStringByBackReferenceAndLookaroundTest.main(RegexReplaceStringByBackReferenceAndLookaroundTest.java:55)
Process finished with exit code 1
(참고) maven repository에서 testImpliment로 복사되어서 test 이하 패키지에서 구현하였으나 실행이 되지 않았음.
- gradle dependencies 샘플
dependencies {
// jmh bachmarking
implementation 'org.openjdk.jmh:jmh-core:1.37'
implementation 'org.openjdk.jmh:jmh-generator-annprocess:1.37'
}
참조: https://www.baeldung.com/java-regex-replace-strings-back-reference-vs-lookaround
참고: https://github.com/openjdk/jmh
'Development > Java' 카테고리의 다른 글
[JAVA] "Could not create the Java Virtual Machine" 에러 수정 방법 (2) | 2024.09.02 |
---|---|
[JAVA] 배열의 정렬된 인덱스 가져오기(배열 보존) (0) | 2024.08.13 |
[JAVA] Iterator.forEachRemaining()과 Iterable.forEach()의 차이점 (0) | 2024.08.12 |
[JAVA] 배열에서 0에 가장 가까운 숫자 찾기 (0) | 2024.08.07 |
[JAVA] CLOB과 String(문자열)간의 변환 (0) | 2024.08.07 |