[Java] List 형을 String 형으로 변환하는 여러가지 방법

    List 형으로 String으로 변환하는 방법이야 for 문으로 list Loop를 돌면서 String에 Add하고 처리하는 고전적인 방법이 있는 반면 stream 을 활용하여 처리하는 방법, 외부 라이브러리를 사용하여 처리하는 방법등 여러가지 방법이 존재할 것이다.


    본 강의는 List를 Loop돌아서 String에 Add하는 방법 뿐만 아니라 외부 라이브러리와 Stream 기능을 써서 처리하는 2가지 방법도 추가하여 포스팅 하고자 한다



    실험 방법


    우선 랜덤형으로 List<String> 형 데이터를 N개 생성한다. 랜덤 소스는 아래와 같다.


    /**

     * int를 받아서, 랜덤 알파벳을 출력함

     * 

     * @param wordLength

     * @return

     */

    public String randomAlphaWord(int wordLength) {

    Random r = new Random();

    StringBuilder sb = new StringBuilder(wordLength);

    for(int i = 0; i < wordLength; i++) {

    char tmp = (char) ('a' + r.nextInt('z' - 'a'));

    sb.append(tmp);

    }

    return sb.toString();

    }


    int 값을 호출하면, 소문자의 알파벳 조합으로 리턴을 하는 메소드이다. 테스트는 100개, 1000개, 10000개를 만들 것이며, 각각의 성능을 테스트 해보고자 한다.



    고전적인 3가지 방식


    List<String> 데이터를 loop를 돌아 데이터를 세팅한다. 

    startTime = System.currentTimeMillis();


    String result3 = "";

    for(String str : list) {

    result3 += str;

    }


    endTime = System.currentTimeMillis();

    System.out.println(result3.length() + " " + (endTime-startTime));


    startTime = System.currentTimeMillis();


    StringBuffer result4 = new StringBuffer();

    for(String str : list) {

    result4.append(str);

    }


    endTime = System.currentTimeMillis();

    System.out.println(result4.toString().length() + " " + (endTime-startTime));


    startTime = System.currentTimeMillis();


    StringBuilder result5 = new StringBuilder();

    for(String str : list) {

    result5.append(str);

    }


    endTime = System.currentTimeMillis();

    System.out.println(result5.toString().length() + " " + (endTime-startTime));


    각각의 알고리즘 길이의 시간을 체크하기 위해서 startTime과 endTime을 currentTimeMillis로 세팅을 하며, 알고리즘별 시간을 체크하게 방식이다.



    Stream을 사용하는 방법


    startTime = System.currentTimeMillis();


    String result = list.stream()

    .map(n -> String.valueOf(n))

    .collect(Collectors.joining());

    endTime = System.currentTimeMillis();


    System.out.println(result.length() + " " + (endTime-startTime));


    문자열을 단순히 합치는 로직은 Collectors.joining() 처럼 아무런 인자값을 주지 않았을 때의 방식이고, joining 메소드 안에 인자값으로 문자열의 앞뒤, 그리고 연결될때의 값 등을 세팅할 수 있다.


    예를 들어, .collect(Collectors.joining("-", "{", "}")); 이렇게 지정하면 앞뒤 괄호가 들어가며 문자열 사이사이 하이픈이 포함된다.



    외부 라이브러리를 사용하는 방법


    startTime = System.currentTimeMillis();


    String result2 = StringUtils.join(list, "");

    endTime = System.currentTimeMillis();


    System.out.println(result2.length() + " " + (endTime-startTime));


    commons-lang3 라이브러리를 지정하면, StringUtils를 사용할 수 있는데 join 메소드로 문자열을 합치는 방법을 제공한다.


    위에는 더블 쿼테이션을 2개 사용했지만 예를 들어 (list, ","); 라고 하면 문자열과 문자열을 연결할 때 콤마로 연결할 수 있는 기능을 제공한다.



    성능 테스트


    List<String> list = new ArrayList<String> ();

    long startTime = 0;

    long endTime = 0;


    // 우선 랜덤형으로 n개의 데이터를 세팅

    for(int i = 0; i < 10000; i++) {

    list.add(randomAlphaWord(30));

    }


    startTime = System.currentTimeMillis();

    String result2 = StringUtils.join(list, "");

    endTime = System.currentTimeMillis();

    System.out.println(result2.length() + " " + (endTime-startTime));


    startTime = System.currentTimeMillis();

    String result = list.stream()

    .map(n -> String.valueOf(n))

    .collect(Collectors.joining());

    endTime = System.currentTimeMillis();

    System.out.println(result.length() + " " + (endTime-startTime));


    startTime = System.currentTimeMillis();

    String result3 = "";

    for(String str : list) {

    result3 += str;

    }

    endTime = System.currentTimeMillis();

    System.out.println(result3.length() + " " + (endTime-startTime));


    startTime = System.currentTimeMillis();

    StringBuffer result4 = new StringBuffer();

    for(String str : list) {

    result4.append(str);

    }

    endTime = System.currentTimeMillis();

    System.out.println(result4.toString().length() + " " + (endTime-startTime));


    startTime = System.currentTimeMillis();

    StringBuilder result5 = new StringBuilder();

    for(String str : list) {

    result5.append(str);

    }

    endTime = System.currentTimeMillis();

    System.out.println(result5.toString().length() + " " + (endTime-startTime));


    N개의 데이터는 100개, 1000개, 10000개를 비교해보았다. 


    우선, 100개만 세팅하였을 경우

    3000 19

    3000 47

    3000 1

    3000 0

    3000 0


    Stream 방식이 47ms가 걸렸으며, 2번째로 StringUtils을 사용하는 방식이 19ms나 걸렸다. 100개만 문자열을 연결했을 뿐인데 이정도 속도가 걸린 것은 매우 비효율적이라는 얘기이다.


    1,000개를 세팅 했을 경우

    30000 27

    30000 57

    30000 43

    30000 0

    30000 1


    의외의 결과가 나왔다. 100개와 1000개의 속도 차이가 크게 나지 않았다 갑자기 String 방식으로 데이터를 세팅하는 속도가 확 튀었다.


    10,000개를 세팅 했을 경우

    300000 23

    300000 49

    300000 1807

    300000 2

    300000 1


    의외의 결과가 나왔다. 10000개의 문자열을 합치는 알고리즘인데 외부 라이브러리와 Stream을 사용하는 방식의 속도 차이가 나지 않는다는 것이다. 즉 일정한 속도가 계속 나게 된다. 그에 반면 String 형으로 데이터를 세팅하는 것은 1.8초라는 시간이 걸리게 되었다. 


    아무래도 String에 문자열을 추가하는 방식이 StringBuffer와 StringBuilder에 비해서 비효율적이기 때문이라는 것을 알 수 있다.


    100,000개를 세팅 했을 경우

    3000000 43

    3000000 99

    3000000 164349

    3000000 7

    3000000 6


    100,000개는 Stream이든 외부 라이브러리든 속도의 영향을 어느 정도 받는다는 것을 알 수 있다. String을 추가하는 것은 말도 안되게 튀어서 도저히 사용할 수 없는 지경이다.



    결론


    문자열이 많고 한번의 분석(배치성)으로 작업을 할 경우, String에 데이터를 세팅하는 것은 바보 같은 행동이며, 그 외의 작업은 StringBuffer, StringBuilder, Stream, StringUtils 모두 비슷한 성능을 나타낸다고 볼 수 있다.


    빈번하게 데이터를 병합해야 하는 시스템이라면, Stream과 StringUtils은 속도 이슈가 어느정도 발생할 가능성이 있다. 예를 들어 Open API를 호출하고 100ms 안에 데이터를 주지 못할 경우 Connection을 끊어버리는 로직이 존재한다고 가정할 때, 빈번하게 커넥션이 끊어질 확률이 존재한다.


    가장 무난한 것은 역시나 고전적인 방식을 쓰되, 되도록이면 StringBuffer나 StringBuilder를 활용하도록 하는 방식이 좋을 것이다.


    댓글

    Designed by JB FACTORY