알고리즘 문제를 풀다가 반복문을 이용해 문자열을 합치는 부분이 있었다. 간단한 문제여서 String으로 문자열을 합쳤었는데 다른 사람들의 풀이를 보니 StringBuilder로 문제를 풀었다. 해당 문제를 String과 StringBuilder로 풀어봤는데 속도 차이가 많이 나서 어떤 차이 때문에 그런지 확인해보려고 한다. 

String으로 풀었을 때
StringBuilder로 풀었을 때


String

  • String은 불변(immutable)한 객체이며, 한 번 생성되면 내용을 변경할 수 없으며 길이가 정해져 있다. 이러한 특성 때문에 멀티스레드 환경에서 사용하기 적절하다. 
    public class TEST {
        public static void main(String[] args){
            String str = "ABC";
            System.out.println("ABC : " + str.hashCode());
    
            str = "AAA";
            System.out.println("AAA : " + str.hashCode());
        }
    }​


    위 코드는 동일한 str 변수이지만, ABC와 AAA를 가리키는 참조값이 다른 것을 확인할 수 있다.
  • 위 결과에서 알 수 있는 것처럼, 문자열 조작(추가, 자르기 등) 시 새로운 객체를 생성해 조작된 문자열을 할당하고 기존 String 객체는 GC(Garbage Collection)에 의해 수거된다. 
    public class TEST {
        public static void main(String[] args){
            String str = "ABC";
            System.out.println("ABC : " + str.hashCode());
    
            str += "AAA";
            System.out.println("ABCAAA : " + str.hashCode());
        }
    }
    단, JDK 1.5 이상부터는 위와 같이 + 로 문자열을 합칠 경우 컴파일러 단에서 자동으로 StringBuilder로 변경해준다고 한다. 

  • String 객체는 Comparable 인터페이스를 상속하고 있기 때문에 equals 메서드로 문장이 동일한지 확인 가능하지만 StringBuilder는 Comparable 인터페이스를 상속하지 않기 때문에 equals 메서드가 없으며 비교 메서드가 따로 존재하지 않는다. 
  • 새로운 String 객체를 만들고 값을 할당할 때 JVM(Java Virtual Machine)은 내부적으로 가지고 있는 string pool에서 같은 값이 있는지 확인한다. 같은 값이 있을 경우, pool에서 해당 객체의 참조값을 반환하며 값이 없을 경우엔 pool에 값을 생성한 후 참조값을 반환한다. 그래서 각 다른 String 객체에 같은 값을 할당하면 가리키는 값이 동일하며 똑같은 값을 두 번 할당하지 않기 때문에 메모리 또한 절약할 수 있다.
    public class TEST {
        public static void main(String[] args){
            String str = "ABC";
            String str2 = "ABC";
            System.out.println("str : " + str.hashCode());
            System.out.println("str2 : " + str2.hashCode());
        }
    }​

StringBuilder/StringBuffer

  • StringBuilder는 가변(mutable)한 객체이며 내용의 변경이 가능하며, 내용을 변경할 경우 내부에서 내용의 길이를 변경한다. 이러한 특성 때문에 멀티스레드에서 사용하기에는 적절하지 않다. 
    public class TEST {
        public static void main(String[] args){
            StringBuilder sb = new StringBuilder("ABC");
            System.out.println(sb.toString() + " : " + sb.hashCode());
    
            sb.delete(0, sb.length());
            sb.append("AAA");
            System.out.println(sb.toString() + " : " + sb.hashCode());
        }
    }​

    동일한 sb 변수에 ABC값을 할당한 후 hashCode값과 sb 변수에 할당되어있는 값을 지우고 append() 메서드로 AAA 값을 할당한 결과 동일한 참조값을 가지고 있음을 볼 수 있다.

  • StringBuilder는 문자열 조작 시 새로운 객체를 할당하지 않으며 단순히 내부에서 문자열의 길이를 변경해 사용한다. 
    public class TEST {
        public static void main(String[] args){
            StringBuilder sb = new StringBuilder("ABC");
            System.out.println(sb.toString() + " : " + sb.hashCode());
    
            sb.append("AAA");
            System.out.println(sb.toString() + " : " + sb.hashCode());
        }
    }​

  • String에서 언급했지만 StringBuilder는 Comparable 인터페이스를 상속하지 않기 때문에 equals 메서드를 이용한 비교가 불가능하다. 따라서 아래와 같이 String 형식으로 변환 후 비교를 진행해야 한다.
    public class TEST {
        public static void main(String[] args){
            StringBuilder sb = new StringBuilder("ABC");
            StringBuilder sb2 = new StringBuilder("ABC");
    
            if (sb.toString().equals(sb2.toString())){
                System.out.println("sb, sb2 is equal");
            }
            else{
                System.out.println("sb, sb2 is not equal");
            }
        }
    }​
  • String과 다르게 StringBuilder는 객체에 동일한 값을 할당하더라도 참조값이 서로 다름을 확인할 수 있다.
    public class TEST {
        public static void main(String[] args){
            StringBuilder sb = new StringBuilder("ABC");
            System.out.println("sb : " + sb.hashCode());
    
            StringBuilder sb2 = new StringBuilder("ABC");
            System.out.println("sb2 : " + sb2.hashCode());
        }
    }​

     
  • StringBuffer는 StringBuilder와 사용하는 메서드가 같은데, 차이점이라면 synchronized 키워드가 존재하여 멀티스레드 환경에서 안전하다고 한다. 

String vs StringBuilder/StringBuffer 성능 비교

동일한 문자를 100만 번 반복했을 때의 속도를 비교해보자.

public class TEST {

    private static final int REPEAT_COUNT = 1000000;

    public static void main(String[] args){
        long start, end;

        start = System.currentTimeMillis();
        StringAppend();
        end = System.currentTimeMillis();
        System.out.println("StringAppend : " + (end - start) + "ms");

        start = System.currentTimeMillis();
        StringBuilderAppend();
        end = System.currentTimeMillis();
        System.out.println("StringBuilderAppend : " + (end - start) + "ms");

        start = System.currentTimeMillis();
        StringBufferAppend();
        end = System.currentTimeMillis();
        System.out.println("StringBufferAppend : " + (end - start) + "ms");
    }

    public static void StringAppend(){
        String str = "";
        for(int i=0; i<REPEAT_COUNT; i++){
            str += '!';
        }
    }

    public static void StringBuilderAppend(){
        StringBuilder sb = new StringBuilder();
        for(int i=0; i<REPEAT_COUNT; i++){
            sb.append("!");
        }
    }

    public static void StringBufferAppend(){
        StringBuffer sb = new StringBuffer();
        for(int i=0; i<REPEAT_COUNT; i++){
            sb.append("!");
        }
    }
}

결과에서 확실하게 알 수 있듯이 String을 + 문자로 계속 추가하게 되면 객체의 생성, 삭제가 계속해서 발생하게 되며 GC가 개입하기 때문에 단순히 문자열을 연결할 경우 속도면에서 매우 불리하다. 그리고 StringBuffer를 이용한 값이 StringBuilder보다 조금 더 오래 걸리는데, 그 이유가 StringBuffer는 내부적으로 thread-safe 를 유지하기 위해 별도의 작업을 진행하기 때문에 시간차이가 난다고 한다.

 

https://coderanch.com/t/666742/java/understand-StringBuffer-thread-safe-StringBuilder

 

How to understand StringBuffer is thread safe and StringBuilder is not by example output? (Beginning Java forum at Coderanch)

 

coderanch.com

위 주소에는 StringBuilder와 StringBuffer를 Thread 환경에서 사용했을 때 어떻게 다른지 알 수 있는 예제코드가 댓글에 달려있다. 

 


결론

1. 간단한 문자열 조작은 단순히 String 객체를 이용해 +로 조작하는 것이 가독성에 좋다.

2. 빈번한 문자열 조작이 필요할 경우 StringBuilder나 StringBuffer를 이용하는 것이 성능상에 유리하다.

3. 멀티스레드 환경에서 thread-safe 해야한다면 StringBuffer를, 그렇지 않다면 StringBuilder를 이용하는 것이 성능에 유리하다.

 

'0x10. 프로그래밍 > 0x11. Java' 카테고리의 다른 글

[JAVA] 정규표현식  (0) 2021.07.11

정규표현식이란?


  • 특정 문자열을 처리할 때 사용하는 방법 중 하나
  • 특정 문자열에서 패턴을 검색하거나 치환할 때 사용되며 기존에는 코드로 직접 작성해야 했으나, 이 정규표현식을 이용하면 간단하게 문자열을 처리할 수 있음
    ex) 이메일 주소 및 핸드폰 번호 검증 등
  • 입력이 불가능한 문자들이 들어있는지 사전에 확인하여 보안상으로도 유용함
    ex) SQL에서 주석으로 표현되는 ' -- 와 같은 문자 확인

 

 

Pattern, Matcher 클래스


  • Pattern 클래스
     - 검색 또는 치환할 패턴을 정의(Compile)하는 클래스
  • Matcher 클래스
     - 패턴을 해석해서 문자열이 일치하는지 확인하는 클래스

 

 

 

예제


  • 이메일 주소 검증
    - 이메일 주소는 대부분 test@test.com 형태로, 아이디 부분은 영문 대/소문자, @는 하나이며 주소에는 .이 하나 이상이 들어간다고 가정했다. 이 정보를 기반으로 패턴을 정의하고 검증하는 과정을 거친다.
      더 자세하게 검증 패턴을 만들기 위해서는 직접 구현하거나 검색을 통해 패턴을 알 수 있다.
import java.util.regex.*;

public class 정규표현식 {
    public static void main(String args[]){
        String strPattern = "[a-zA-Z_0-9]+@[a-zA-Z_0-9-]+[.[a-zA-Z_0-9-]]+";

        Pattern p = Pattern.compile(strPattern);

        String email = "test_01@test.co.kr";

        Matcher m = p.matcher(email);

        System.out.println("이메일 검증 : " + m.matches());
    }
}
결과

이메일 검증 : true
  • 문자열 치환
     - 문자열에서 패턴을 이용해 특정 문자열을 치환할 수 있다. 아래 예제는 . 이 2번 이상인 경우 . 한번으로 치환하는 코드이다.
    import java.util.regex.*;
    
    public class 정규표현식 {
        public static void main(String args[]){
            String strPattern = "\\.{2,}";
            String strTmp = "a.bb..ccc...dd..e.";
            String convTmp = strTmp.replaceAll(strPattern, ".");
            
            System.out.println("문자열 치환 : " + convTmp);
        }
    }​
     Matcher 대신 String에서 제공해주는 replaceAll 메소드를 직접 이용해 패턴을 설정하고 치환할 수 있다.
    결과

    문자열 치환 : a.bb.ccc.dd.e.

 

'0x10. 프로그래밍 > 0x11. Java' 카테고리의 다른 글

[JAVA] String, StringBuilder/StringBuffer 차이  (0) 2021.08.13

+ Recent posts