본문 바로가기
개발 언어/자바

String, String format, 그리고 String builder

by Marco Backman 2024. 1. 29.

Java 에는 문자열을 사용할 때 쓰는 String을 선언하는 방법 중, 여러가지가 있지만 대표적으로 일반 String 그리고 String format, String builder를 많이 사용했었는데, 이 세가지의 차이점과 단점, 이점이 무엇이 있는지를 적어보도록 하겠다.


String

전반적으로 많이 사용하는 String 선언 방식인데 character들의 배열형태로 이루어지며 Immutable이다. 즉, 한번 생성되면 절대로 수정 될 수가 없다. 겉 보기로는 수정이 되는 것 같아 보이지만, String을 선언할 때 String Pool에서 선언된 string열을 꺼내다가 사용하기 때문에 결국에는 Immutable이다. 전에 자바 메모리 모델을 설명할 때 String Heap메모리와 연관 지으며 String이 선언되면 어떻게 저장되는지 글을 썼었다.

https://marcobackman.tistory.com/15 

 

Java 의 메모리 모델

자바에는 여러가지 메모리 모델이 융합되어 사용되는데 각 메모리 구역에 따라 필요한 역활을 수행하기 때문에 메모리 구역을 나눈 것이다. 대표적으로 메모리 구역은 크게 이렇게 나뉜다. 물

marcobackman.tistory.com

 

그렇기 때문에 자주 반복되고 방대한 스트링열을 '+' 를 사용해서 큰 스트링열을 제작한다면 퍼포먼스적으로 문제가 생긴다.

 

예를 들면 아래와 같은 스트링 제작 방식은 비 효율적인 방식이다.

public class StringConcat {

    private void concatWithString() {

        String name = "Marco";
        String result = "";
        for (int i = 0; i < 10; i++) {
            result += name + " : " + i + "\n";
        }
        System.out.println(result);

    }


    public static void main(String[] args) {
        StringConcat inst = new StringConcat();
        inst.concatWithString();
    }
}

 

 

실제로 본인이 사용하는 IntelliJ에서 위와 같이 코드를 작성하면 경고도 띄워준다.

IDE의 경고문

 

 

이러한 '+' concatenation이 비효율적인 이유를 자세히 알아보면 실행하는 동안 String 조합을 '+'를 마주할 때마다 새로운 스트링을 만들기 때문이다. 

 

만약 우리가 필요한 스트링 열은 Result, 한 문장 뿐인데 실제로 String Pool에는 한 중장까지 만들어지는 과정에서 접합되는 스트링 조합을 다 만들어 내기 때문이다

 

예를 들면 String Pool안에는 위 코드를 실행 시킨 뒤, 다음과 같은 스트링들 조합들이 저장되어 있을 것이다.

Marco

Marco : 

Marco : 1

Marco : 1
Marco

Marco : 1
Marco :

Marco : 1
Marco : 2

Marco : 1
Marco : 2
Marco

Marco : 1
Marco : 2
Marco :

Marco : 1
Marco : 2
Marco : 3

...

 

그렇지만 우리는 부분적인 스트링 정보는 일반적으로 필요하지가 않고 앞으로도 사용할 이유가 없다.

그리는 아래 결과값만 보고 싶을 뿐이지, 위의 순차적 스트링 조합에 대해는 관심이 없지만 우리는 이미 String Pool에 모든 조합을 저장함으로서 불필요하게 메모리를 사용하고 있는 것이다.

Marco : 0
Marco : 1
Marco : 2
Marco : 3
Marco : 4
Marco : 5
Marco : 6
Marco : 7
Marco : 8
Marco : 9

 

더 확실하게 알아보자, 실제로 String Pool에 이미 String결괏값의 일부분이 선언이 되어있는지 말이다.

 

private void concatWithString() {

    String name = "Marco";
    String result = "";
    for (int i = 0; i < 10; i++) {
        result += name + " : " + i + "\n";
        System.out.println(result.hashCode());
    }
    System.out.println(result);
    System.out.println("Marco : 0\n".hashCode());

}

 

만약 loop안에 있는 스트링 해시 값과 예상되는 String열의 일부의 해시 값을 찍어보고 값이 같다면 이는 이미 '+'를 할 때마다 String Pool에 스트링 값이 개별적으로 저장된다는 말이다.

 

"Marco : 0\n"의 같은 해시값이 출력되었다. 이렇게 우리는 불필요한 스트링을 지속적으로 만들어내고 있던 것이다.

 

그렇다면 어떻게 하면 이 문제점을 피할 수 있을까?

 


String Builder

 

복잡한 스트링 열을 효율적으로 선언하기 좋은 가장 대표적인 라이브러리로 String Builder 가 있다. Builder패턴에 해당되며 chained concatenation이 가능하기 때문에 매우 편리하면서도 유용한 라이브러리이다.

 

빌더 패턴에 대한 자세한 설명글을 참고하길 바란다. https://marcobackman.tistory.com/7

 

소프트웨어 디자인 패턴 - 빌더패턴 Builder Pattern (Creational Design Pattern - 2)

오늘은 소프트웨어 디자인 패턴에 있어 빌더 패턴 (Builder pattern) 을 다룰려고 한다. 빌더 패턴을 직접 구현하지 않았더라도 현업에서는 기본적으로 쓰고 있는 패턴이라고 믿는다. 자바 스프링에

marcobackman.tistory.com

일반 String가 별개로 Mutable이기 때문에. build 나. toString()을 호출하기 전까지는 스트링의 수정 추가가 가능하며, String Pool에다 모든 스트링 조합을 저장하지 않기 때문에 메모리 효율적이다. 추가적으로 속도면에서도 매우 우위를 선점하고 있는데 그 이유는 String 오브젝트를 생성하지 않고 내부 적으로 byte array를 바로 수정하고 직접 조합하기 때문이다.

 

위의 String 기반의 코드를 고치면 다음과 같다

 

private void concatWithString() {

    String name = "Marco";
    StringBuilder result = new StringBuilder();
    for (int i = 0; i < 10; i++) {
        result.append(name).append(" : ").append(i).append("\n");
    }
    System.out.println(result);
}

 

 

 

단점이라면 Synchronized 가 아니기 때문에 동기 문제가 생길 수가 있다. 그러면 완벽한 동기형 스트링을 사용하고 싶으면 다음 라이브러리를 사용하면 된다.

 

String Buffer

 

일반적인 String과 다른 점은 Synchronized 이기 때문에 다중 스레드 환경, 멀티스레딩에서 공유된 스레드 조합 할 때 String Buffer를 쓰는 게 좋다. 그러나 언제나 장점이 있으면 단점이 있는 법, Synchronized 특성상 충돌 케이스를 항상 체크하고 static resource 이기 때문에  String Builder에 비해 속도가 느리다는 단점이 존재하기 때문에 필요할 때 사용해야 한다.

 


 

String format

 

성능과는 별개로 보기 불편한 스트링 선언 코드를 Readable 코드로 만들게 하는 라이브러리이며, 만약 사용빈도가 많다면 일반 String보다 확실히 성능이 나은 편이다. 왜냐하면 Paramatized string을 사용하면 스트링 문자열을 한 번만 제작하기 때문이다.

 

예시를 들자면 Query 문구 같이 하나의 스트링 열이지만 주어지는 변수들이 중간에 많이 껴들어 갈 때 사용 할 수도 있다.

 

 

private void formQuery() {
    String tableName = "TEST_TABLE";
    String fieldName = "testField";
    String value = "testValue";
    //String sql = "Select * from " + tableName + " where " + fieldName + "=\'" + value + "\';";
    String sql = String.format("Select * from %s where %s='%s'", tableName, fieldName, value);
    System.out.println(sql);
}

 

 

그러면 왜 String이 비효율 적이면 왜 필요한가 라는 의문을 던 질 수 있지만

우리가 보통 String 오브젝트를 사용할 때는 비교적 글자 수가 적고 반복 수가 적은 케이스에 사용하던가, 조합된 스트링 열을 저장하는 보관소로도 사용한다. 또한 String Builder와 String format은 문자열의 조합을 도와주지만 결국 마지막에 사용될 때는 스트링 타입으로 반환하기 때문에 String이 필 수적으로 필요한 오브젝트라 할 수 있다.

 

그러기 때문에 String을 선언하고 조합하는 데 있어서 자주 반복되는지, 문장이 길어지고 복잡한 조합이 수반되는지를 잘 생각해 보고 사용해야 한다. 결론은, 가독성(Readability) vs 성능(performance)을 잘 생각해 봐야 한다.

 

 

위의 라이브러리들 말고도 수많은 문자열 기능을 내포한 라이브러리들이 많이 있다. 다음에 기회가 된다면 더 올려 보록 하겠다.