본문 바로가기
Develop/Java

[Java] replaceAll 대신 replace 사용하기

by 연로그 2023. 1. 22.
반응형

🙂 개요

 

String에서 흔히 사용하는 메서드 중에서는 replaceAll라는게 있다. 다들 알다시피 replaceAll은 특정 문자를 다른 문자로 대치할 수 있게 해주는 아주 편리한 메서드이다. 그러다 replaceAll보다는 replace를 사용하는 것이 좋다는 이야기를 듣게 되어 사실인지 살펴보기 위해 해당 포스팅을 작성하게 되었다.

 

 


🤓 간단 테스트

 

 먼저 replace에 대해 흔히 하는 오해를 바로 잡고 가야한다. replaceAll 메서드가 따로 있다보니 replace는 일치하는 첫 부분 또는 일부만 대체해 주는건가? 라고 착각할 수 있다. 예를 들어 A를 B로 대체한다고 가정할 때, AA가 BA로 대체될 거라고 착각한다. 하지만 실제로는 BB로 대체된다. (참고로 여러 일치하는 문자열 중 첫 부분만 대체하고 싶다면 replaceFirst를 사용하면 된다.)

 

 어떤 것이 더 빠르게 동작하는지 간단한 코드를 짜고 실행시켜보았다. Supplier를 잘 모른다면 코드가 잘 이해되지 않을 수 있다. 이 코드에서는 시간 체크하는 코드의 중복을 방지하기 위해 사용했다 정도로만 이해하고, 더 궁금한 사람은 함수형 인터페이스 키워드를 더 공부해보길 바란다.

 

class StringTest {

    @Test
    void test() {
        String text = "Hello World!".repeat(100);
        String text2 = text;

        checkTime(() -> text.replace("o", "0")); // 1. replace 실행 시간 체크
        checkTime(() -> text2.replaceAll("o", "0")); // 2. replaceAll 실행 시간 체크
    }

    // 실행 전후로 시간을 재서 출력해주는 메서드
    private void checkTime(Supplier<String> consumer) {
        long before = System.currentTimeMillis();
        consumer.get();
        long after = System.currentTimeMillis();
        System.out.println(after - before);
    }
}

 

repeat을 통해 text의 길이를 조정하며 실행 시간에 어떤 차이점을 보이는지 테스트해보았다. 아래의 표를 보면 문자열이 길면 길수록 메서드의 실행 속도가 빨라지는 것처럼 보인다. 왜 이런 현상이 나타났는지 확인해보자.

 

repeat 횟수 replace() replaceAll()
100 0 1
100,000 11 23
100,000,000 2534 8186

 

 


🧐 코드 까보기

 

⭐️ replace()

replace 메서드의 핵심 부분만 가져왔다. 살펴보면 문자열의 크기에 따라 반복문이 돌아가는 횟수가 다르다.

public String replace(CharSequence target, CharSequence replacement) {
    String tgtStr = target.toString(); 		// 변경을 원하는 문자열
    String replStr = replacement.toString(); 	// 대치할 문자열
    int j = indexOf(tgtStr); 			// 문자열에서 tgtStr의 첫번째 위치(인덱스)를 찾아온다.
    
    int tgtLen = tgtStr.length();
    int tgtLen1 = Math.max(tgtLen, 1);
    int thisLen = length();

    StringBuilder sb = new StringBuilder(newLenHint);
    int i = 0;
    
    // j가 문자열의 크기보다 작고 다음 대치할 문자의 인덱스가 0보다 클 동안 반복
    do { 
        sb.append(this, i, j).append(replStr);
        i = j + tgtLen;
    } while (j < thisLen && (j = indexOf(tgtStr, j + tgtLen1)) > 0);
    
    return sb.append(this, i, thisLen).toString();
}

 

⭐️ replaceAll

replace에 비해 replaceAll은 굉장히 간단해보인다. 

public String replaceAll(String regex, String replacement) {
    return Pattern.compile(regex).matcher(this).replaceAll(replacement);
}

 

여기서 replaceAll이 성능에서 밀리는 이유가 하나 파악된다. Pattern이 사용되기 때문이다. Pattern 사용을 무조건 지양해야하는 것은 아니지만, 다른 훌륭한 대체 방법이 많은 상황에서 굳이 Pattern을 사용할 필요는 없어보인다.

정규표현식용 Pattern 인스턴스는, 한 번 쓰고 버려져서 곧바로 가비지 컬렉션 대상이 된다. Pattern은 입력받은 정규표현식에 해당하는 유한 상태 머신(finite state machine)을 만들기 때문에 인스턴스 생성 비용이 높다.
ㅡ Effective Java / item 6. 불필요한 객체 생성을 피하라

 

 


🤔 무조건 replace를 사용해야 하나요?

 

무조건 replace를 사용해야하느냐? 라고 묻는다면 아니다. replace는 단순히 문자열을 대체할 수 있기 때문에 복잡한 정규식이 필요한 상황에서라면 적절하지 않다. 예를 들어 숫자를 문자 A로 변경해야하는 상황이라고 가정하고 테스트 코드를 작성해보았다. Pattern의 비용이 큰 것은 어쩔 수 없지만 숫자 중 어느 하나가 빠지지 않았나 체크할 필요도 없고, 코드도 훨씬 깔끔해보인다. 때와 상황에 맞게 잘 사용하라. 

 

@Test
void parseDate() {
    String text = "Hello 12345!".repeat(1000);
    String text2 = text;

    checkTime(() -> text.replace("0", "A")
            .replace("1", "A")
            .replace("2", "A")
            .replace("3", "A")
            .replace("4", "A")
            .replace("5", "A")
            .replace("6", "A")
            .replace("7", "A")
            .replace("8", "A")
            .replace("9", "A"));
    checkTime(() -> text2.replaceAll("\\d", "A"));
}

 

반응형