Welcome to My World (www.dgmayor.com)

소프트웨어/자바 GUI & C# 등...

45. 자바 스프링버퍼 및 초기화 방식

dgmayor 2022. 4. 12. 17:38
728x90

처음 대학에 들어가 c언어를 배우면서 일상에서 쉽게 사용하던 문자열들이 굉장히 어렵게 처리했던것들이 기억난다. char형의 배열로 문자열형태를 만들고 제어했는데, 자바는 String 클래스 하나면 문자열을 저장하고 합치고 필요한 메소드를 사용해 제어가 가능하다.

 

불과 작년까지 String을 자바의 기본 자료형(primitive type)으로 착각하고 문자열처리는 String 클래스로만 가능할 것이라고 생각했던 것에 반성하며 String, StringBuffer, StringBuilder클래스에 대해 정리해본다.

 

String

String 클래스는 일단 변경 불가능한 클래스(immutable)이다.

쉽게 String클래스에 문자열을 넣어 사용하지만 실제로는 다른 언어처럼 char[] 배열 변수를 인스턴스 변수로 받아서 저장이 된다.

final 상수로 String을 처리하였다.(불변 객체 String)

"Test"+ "Sum" = "TestSum" 이런식으로 String으로 쉽게 문자열들을 합쳐왔을텐데, 인스턴스 내의 문자열이 바뀌는게 아닌 TestSum이라는 새로운 문자열이 생성되는 것이다.

 

String의 문자열 비교는 equals()

여기서 우리가 자주 쓰던 조건문의 eqauls() 사용하는 이유가 나오는데, 문자열은 생성할때 새로운 인스턴스를 생성하고 메모리에 저장한다.

 

public class Test {

	public static void main(String[] args) {
		String a = "test1";
		String b = "test";
		String c = "1";
		String d = b + c;
		
		if(a == d) {
			System.out.println("같아요");
		}else {
			System.out.println("같지 않아요");
		}
		
		if(a.equals(d)) {
			System.out.println("같아요");
		}else {
			System.out.println("같지 않아요");
		}
	}
}

같지 않아요 // 같아요

 

당연히 equals로 비교하지 않았기 때문에 else를 탄다는것을 알지만 왜? 가 여기서 이해가 될 것이다.

test라는 문자열을 생성하고 1을 생성 후 둘을 합치면서 새로운 test1이라는 문자열을 만들었기때문에 둘의 생성된 메모리의 주소값은 다르다. 그래서 두 변수의 문자열의 값만 비교하는 eqauls()를 통해 boolean값을 얻는다.

 

계속해서 말하는 내용이지만 결합이나 추출 등의 작업이 나오면 새로운 문자열이 생성된다. 즉, 문자열을 합칠때마다 새로운 문자열을 만들기 위해 인스턴스를 생성하고 메모리를 잡아먹는다.

문자열의 결합, 추출 등의 작업이 필요하다면 String이 아닌 StringBuffer를 통해 처리하는것이 시스템상 좋을 것이다. 왜? 인스턴스를 매번 생성하고 메모(간단하게 합치고 추출정도는 현대의 컴퓨터 연산과 메모리에 큰 영향이 없으므로 상관 없을것같다.)

 

 

 

StringBuffer

StringBuffer는 불변객체가 아니다. 내부적으로 buffer를 가지고 있는데,

StringBuffer 인스턴스를 생성할때는 적절한 길이의 char형 배열이 생성되고, 이 배열은 문자열을 저장하고 편집할때 사용하는 buffer가 된다.

 

기본 사이즈는 16

여기서 적절한 길이를 지정해야 하는 이유는, 수정하는 문자열이 버퍼의 길이를 넘어가면 버퍼의 길이를 늘려주는 작업이 추가되기 때문이다. 배열은 길이를 바꿀수 없기때문에 이전에 작업하던 배열의 값을 복사해서 넣어준다.

 

StringBuffer의 문자열 비교

StringBuffer의 equals는 String의 equals와는 다르게 (==) 비교 연산이라 문자열 비교시 원하는 결과를 얻을 수 없다.

toString()을 통해 String인스턴스화를 거친 후 비교를 한다.

public class Test {

	public static void main(String[] args) {
		StringBuffer sb = new StringBuffer("test1");
		StringBuffer sb2 = new StringBuffer("test1");
		if(sb.equals(sb2)) {
			System.out.println("같아요");
		}else {
			System.out.println("같지 않아요");
		}
		
		String str1 = sb.toString();
		String str2 = sb.toString();
		if(str1.equals(str2)) {
			System.out.println("같아요");
		}else {
			System.out.println("같지 않아요");
		}
	}
}

같지 않아요 // 같아요

 

StringBuffer의 문자열 합치기, 추출, 제거하기

문자열 합치기

StringBuffer에는 append()메소드가 존재한다. 해당 메소드를 통해 문자열을 추가할 수 있다.

StringBuffer sb = new StringBuffer("a");
sb.append("b"); //ab
sb.append("c").append("d").append("e"); //abcde
System.out.println(sb); //abcde

"abcde"

 

위 캡처처럼 계속 연결해서 사용할 수도 있다.

 

 

문자열 제거

delete(), deleteCharAt() 메소드를 통해 문자열을 지우거나 특정 문자를 제거할 수도 있다.

StringBuffer sb = new StringBuffer("abcde");
sb.delete(0, 2);
System.out.println(sb); //cde

"cde"

"abd"

 

 

문자열 추출

String의 substring의 StringBuffer에도 존재한다. 사용법은 동일하다.

StringBuffer sb = new StringBuffer("hello world!");
String str1 = sb.substring(6); //world!
String str2 = sb.substring(0, 5); //hello

"hello"

 

 

문자열 역순처리

reverse()메소드는 문자열을 거꾸로 나열할 수있다.

StringBuffer sb = new StringBuffer("!dlrow olleh");
sb.reverse(); //reverse
System.out.println(sb); //hello world!

문자열 역순처리로 hello world! 출력

 

 

 

StringBuilder

StringBuilder는 기본적으로 사용하는 메소드나 사용법은 동일하여 서로 호환이 됩니다.(append(), delete(), reverse() 등)

StringBuffer와의 차이점은 동기화를 하는지 않하는지의 차이인데

StringBuffer - 동기화O // StringBuilder - 동기화X

싱글쓰레드 환경에서의 개발이라면 StringBuilder를 멀티쓰레드 환경이라면 동기화처리가된 StringBuffer를 사용하면 됩니다.

즉, StringBuffer의 동기화 처리를 빼고 만들어진 클래스가 StringBuilder입니다.

 

그럼 무시하고 무조건 StringBuffer만 쓰면 되는것이 아니냐? 라고 물어볼 수도 있지만...

동기화처리는 시스템의 불필요하게 성능만 느리게 하므로 차이를 알고 사용하는게 시스템 성능향상에 도움이 될 것입니다.

 

 

 

데이터 분석을 많이 하거나, 대용량의 텍스트를 저장하기에 상당히 유용한 객체로 StringBuffer가 있다. 값을 한번에 저장하는 방법은 String으로 선언된 변수에 한번에 값을 넣는 방법이겠지만, 사이즈가 좀 큰 문자열은 한번에 넣기가 쉽지가 않다. 예를 들어 csv를 만든다고 할 때 그때그때 bw.write로 파일을 내리는 것보다 StringBuffer에 일정만큼 저장한 후 한번에 bw.write에 값을 전달하는 것이 훨씬 속도면에서 유리하다.

 

그러나 StringBuffer에서 데이터를 새로 담을 때, 아무 생각없이 new StringBuffer()로 계속된 초기화를 한적이 있는데 우연찮게 찾아본 문서를 보고 이 부분에 대해서 좀 더 고민할 필요가 있을 것 같아 포스팅을 공유해보고자 한다.

 

StringBuffer 초기화 3가지 방법

 

new StringBuffer() 방식

우선 남들은 보편적으로 사용하고 있는지 모르겠으나, 나는 이 방식을 애용 했었다. GC야 튜닝을 워낙 잘 해서인지 CPU Load가 튀는 적도 없었고 대용량 서비스를 런칭해도 아무런 문제가 없었으나, 돌다리도 두들겨보라고 앞으로 사용을 자제하거나 API 서비스가 아닌 Back단에서 Analyze 하는 쪽에서나 사용하는 것이 좋을 것 같다. 

 

package com.tistory.needjarvis;

public class Main {

    public static void main(String[] args) {
        StringBuffer sb = new StringBuffer();
        for(int i = 0; i < 1000000; i++) {
            sb.append("문자열 ").append(i);
        }
        System.out.println("string buffer 사이즈 -> " + sb.length());
        System.out.println("string buffer Clear");
        long startTime = System.currentTimeMillis();
        sb = new StringBuffer();
        sb.append("새로운 결과");
        System.out.println("string buffer 사이즈 -> " + sb.length());
        System.out.println("elapsed -> " + (System.currentTimeMillis() - startTime) +"(ms)");
    }
}


# 실행결과
string buffer 사이즈 -> 9888890
string buffer Clear
string buffer 사이즈 -> 6 새로운 결과
elapsed -> 0(ms)

아무래도 새로 초기화를 하다보니 다른 방식보다 심플하고 가장 빠를 것 같다 생각하고 있었는데 해외 프로그램 사이트에서는 다음과 같은 것을 경고하고 있다.

 

Here, new StringBuffer() creates a new string buffer object and assigns the previous variable to the new objects. In this case, the previous object will be there. But it won't be accessible so it will be garbage collected.
Since, every time instead of clearing the previous string buffer, a new string buffer is created. So it is less efficient in terms of performance.

위 내용을 보면 새 개체를 만들게 되면서 이전 개체가 존재하게 되며, 이를 garbase collected 즉 GC가 실행되어 수거하게 된다고 나온다. 만약 메모리를 많이 사용하는 경우 GC가 빈번히 발생할 수 있다는 점이며, 성능 측면에서도 좋지 않다 한다. 즉, 한마디로 사용을 자제하라는 것이다.

 

 

 

 

setLength로 초기화

StringBuffer의 Length를 0으로 해서 초기화를 하는 방법도 있다.

package com.tistory.needjarvis;

public class Main {

    public static void main(String[] args) {
        StringBuffer sb = new StringBuffer();
        for(int i = 0; i < 1000000; i++) {
            sb.append("문자열 ").append(i);
        }
        System.out.println("string buffer 사이즈 -> " + sb.length());
        System.out.println("string buffer Clear");
        long startTime = System.currentTimeMillis();
        sb.setLength(0);
        System.out.println("string buffer 사이즈 -> " + sb.length());
        System.out.println("elapsed -> " + (System.currentTimeMillis() - startTime) +"(ms)");
    }
}


# 실행결과
string buffer 사이즈 -> 9888890
string buffer Clear
string buffer 사이즈 -> 6 새로운 결과
elapsed -> 0(ms)

length를 조절해서도 이렇게 쉽게 클리어를 해준다.

 

Here, the setLength() method changes the character sequences present in StringBuffer to a new character sequence. And, set the length of the new character sequence to 0.
Hence, the older character sequence is garbage collected.

다만 이 방법도 만능은 아니라 이전 문자는 garbage collected 된다고 나와 있다. 다만 방식이 심플해서 새로운 객체를 생성하는 new StringBuffer보다 효율적 인것으로 보이며, 코드 라인도 심플해진다.

 

Delete() 사용

package com.tistory.needjarvis;

public class Main {

    public static void main(String[] args) {
        StringBuffer sb = new StringBuffer();
        for(int i = 0; i < 1000000; i++) {
            sb.append("문자열 ").append(i);
        }
        System.out.println("string buffer 사이즈 -> " + sb.length());
        System.out.println("string buffer Clear");
        long startTime = System.currentTimeMillis();
        sb.delete(0, sb.length());
        sb.append("새로운 결과");
        System.out.println("string buffer 사이즈 -> " + sb.length() + " " + sb.toString());
        System.out.println("elapsed -> " + (System.currentTimeMillis() - startTime) +"(ms)");
    }
}

# 실행결과
string buffer 사이즈 -> 9888890
string buffer Clear
string buffer 사이즈 -> 6 새로운 결과
elapsed -> 0(ms)

그리고 대망의 Delete 방법... 이 방식은 장단점이 명확히 나뉘어지지만 솔직히 내부적으로 테스트 해봤을 때는 장점만 있는 것 같다.

In the above example, we have used the delete() method of the StringBuffer class to clear the string buffer.
Here, the delete() method removes all the characters within the specified index numbers.

일단 설명을 보면 GC가 발동되지 않는 것을 알 수 있다. 이전 데이터를 메모리에 저장하지 않고 명확하게 "삭제"하는 방식이라 GC가 발생되지 않는 것으로 파악된다. 결국 API와 같은 서비스에서 일정한 속도가 중요한 경우 GC를 최소화 시키는 것이 중요한데 그럴때에는 new StringBuffer와 같이 GC를 유도하는 방식보다 delete로 GC 자체를 호출하지 않는 방식이 훨씬 효율적인 것 같다.

 

물론 후자의 방식은 어느정도 CPU를 사용하겠지만, Java는 GC만 뜨지 않는다면 API와 같은 서비스에서는 무조건 GC가 최소화하는 것이 좋을 것이다.

 

 

참고자료

https://www.programiz.com/java-programming/examples/clear-stringbuffer



출처: https://needjarvis.tistory.com/674 [자비스가 필요해]
728x90