오늘의 잔디
오늘의 공부
내일 개강이라니
개강이라니!!!
String 클래스의 단점불변인 String 클래스에도 단점이 있다. 다음 예를 보자. 참고로 실제로 작동하는 코드는 아니다.
두 문자를 더하는 경우 다음과 같이 작동한다.
"A" + "B"
String("A") + String("B") //문자는 String 타입이다.
String("A").concat(String("B"))//문자의 더하기는 concat을 사용한다.
new String("AB") //String은 불변이다. 따라서 새로운 객체가 생성된다.
불변인 String 의 내부 값은 변경할 수 없다. 따라서 변경된 값을 기반으로 새로운 String 객체를 생성한다.
더 많은 문자를 더하는 경우를 살펴보자.
String str = "A" + "B" + "C" + "D";
String str = String("A") + String("B") + String("C") + String("D");
String str = new String("AB") + String("C") + String("D");
String str = new String("ABC") + String("D");
String str = new String("ABCD");
String 클래스가 추가로 생성된다.new String("AB") ,new String("ABC") 는 사용되지 않는다. 최종적으로 만들어진 new String("ABCD") 만 사용된다.new String("AB") ,new String("ABC") 는 제대로 사용되지도 않고, 이후불변인 String 클래스의 단점은 문자를 더하거나 변경할 때 마다 계속해서 새로운 객체를 생성해야 한다는 점이다.
문자를 자주 더하거나 변경해야 하는 상황이라면 더 많은 String 객체를 만들고, GC해야 한다. 결과적으로 컴퓨터의 CPU, 메모리 자원을 더 많이 사용하게 된다. 그리고 문자열의 크기가 클수록, 문자열을 더 자주 변경할수록 시스템의 자원을 더 많이 소모한다.
참고: 실제로는 문자열을 다룰 때 자바가 내부에서 최적화를 적용하는데, 이 부분은 뒤에서 다룬다.
문제를 해결하는 방법은 단순하다. 바로 불변이 아닌 가변 String 이 존재하면 된다. 가변은 내부의 값을 바로 변경
하면 되기 때문에 새로운 객체를 생성할 필요가 없다. 따라서 성능과 메모리 사용면에서 불변보다 더 효율적이다.
이런 문제를 해결하기 위해 자바는 StringBuilder 라는 가변 String 을 제공한다. 물론 가변의 경우 사이드 이펙
트에 주의해서 사용해야 한다.
StringBuilder 는 내부에 final 이 아닌 변경할 수 있는 byte[] 을 가지고 있다.
public final class StringBuilder {
char[] value;// 자바 9 이전
byte[] value;// 자바 9 이후
//여러 메서드
public StringBuilder append(String str) {...}
public int length() {...}
...
}
(실제로는 상속 관계에 있고 부모 클래스인 AbstractStringBuilder 에 value 속성과 length() 메서드가 있다.)
실제 StringBuilder 를 어떻게 사용하는지 확인해보자.
package lang.string.builder;
public class StringBuilderMain1_1 {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
sb.append("A");
sb.append("B");
sb.append("C");
sb.append("D");
System.out.println("sb = " + sb);
sb.insert(4, "Java");
System.out.println("insert = " + sb);
sb.delete(4, 8);
System.out.println("delete = " + sb);
sb.reverse();
System.out.println("reverse = " + sb);
//StringBuilder -> String String string = sb.toString();
System.out.println("string = " + string);
}
}
StringBuilder 객체를 생성한다.append() 메서드를 사용해 여러 문자열을 추가한다.insert() 메서드로 특정 위치에 문자열을 삽입한다.delete() 메서드로 특정 범위의 문자열을 삭제한다.reverse() 메서드로 문자열을 뒤집는다.toString 메소드를 사용해 StringBuilder 의 결과를 기반으로 String 을 생성해서 반환한다.실행 결과
sb = ABCD
insert = ABCDJava
delete = ABCD
reverse = DCBA
string = DCBA
String 은 불변하다. 즉, 한 번 생성되면 그 내용을 변경할 수 없다. 따라서 문자열에 변화를 주려고 할 때마다 새로운 String 객체가 생성되고, 기존 객체는 버려진다. 이 과정에서 메모리와 처리 시간을 더 많이 소모한다.StringBuilder 는 가변적이다. 하나의 StringBuilder 객체 안에서 문자열을 추가, 삭제, 수정할 수 있으며, 이때마다 새로운 객체를 생성하지 않는다. 이로 인해 메모리 사용을 줄이고 성능을 향상시킬 수 있다.StringBuilder 는 보통 문자열을 변경하는 동안만 사용하다가 문자열 변경이 끝나면 안전한(불변) String 으로
변환하는 것이 좋다.
자바 컴파일러는 다음과 같이 문자열 리터럴을 더하는 부분을 자동으로 합쳐준다.
문자열 리터럴 최적화
컴파일 전
String helloWorld = "Hello, " + "World!";
컴파일 후
String helloWorld = "Hello, World!";
따라서 런타임에 별도의 문자열 결합 연산을 수행하지 않기 때문에 성능이 향상된다.
String 변수 최적화
문자열 변수의 경우 그 안에 어떤 값이 들어있는지 컴파일 시점에는 알 수 없기 때문에 단순하게 합칠 수 없다.
String result = str1 + str2;
이런 경우 예를 들면 다음과 같이 최적화를 수행한다. (최적화 방식은 자바 버전에 따라 달라진다.)
String result = new StringBuilder().append(str1).append(str2).toString();
참고: 자바 9부터는
StringConcatFactory를 사용해서 최적화를 수행한다.
이렇듯 자바가 최적화를 처리해주기 때문에 지금처럼 간단한 경우에는 StringBuilder 를 사용하지 않아도 된다. 대신에 문자열 더하기 연산( + )을 사용하면 충분하다.
다음과 같이 문자열을 루프안에서 문자열을 더하는 경우에는 최적화가 이루어지지 않는다.
package lang.string.builder;
public class LoopStringMain {
public static void main(String[] args) { long startTime = System.currentTimeMillis();
String result = "";
for (int i = 0; i < 100000; i++) {
result += "Hello Java ";
}
long endTime = System.currentTimeMillis();
System.out.println("result = " + result);
System.out.println("time = " + (endTime - startTime) + "ms");
}
}
왜냐하면 대략 다음과 같이 최적화 되기 때문이다. (최적화 방식은 자바 버전에 따라 다르다)
String result = "";
for (int i = 0; i < 100000; i++) {
result = new StringBuilder().append(result).append("Hello Java
").toString();
}
반복문의 루프 내부에서는 최적화가 되는 것 처럼 보이지만, 반복 횟수만큼 객체를 생성해야 한다.
반복문 내에서의 문자열 연결은, 런타임에 연결할 문자열의 개수와 내용이 결정된다. 이런 경우, 컴파일러는 얼마나 많은 반복이 일어날지, 각 반복에서 문자열이 어떻게 변할지 예측할 수 없다. 따라서, 이런 상황에서는 최적화가 어렵다.
StringBuilder 는 물론이고, 아마도 대략 반복 횟수인 100,000번의 String 객체를 생성했을 것이다.
실행 결과
result = Hello Java Hello Java ....
time = 2490ms
100000 회 더했을 때 약 2.5초가 걸렸다.이럴 때는 직접 StringBuilder 를 사용하면 된다.
package lang.string.builder;
public class LoopStringBuilderMain {
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100000; i++) {
sb.append("Hello Java ");
}
String result = sb.toString();
long endTime = System.currentTimeMillis();
System.out.println("result = " + result);
System.out.println("time = " + (endTime - startTime) + "ms");
}
}
실행 결과
result = Hello Java Hello Java ....
time = 3ms
100000 회 더했을 때 약 0.003초가 걸렸다.정리
문자열을 합칠 때 대부분의 경우 최적화가 되므로 + 연산을 사용하면 된다.
참고: StringBuilder vs StringBuffer
StringBuilder와 똑같은 기능을 수행하는StringBuffer클래스도 있다.
StringBuffer는 내부에 동기화가 되어 있어서, 멀티 스레드 상황에 안전하지만 동기화 오버헤드로 인해 성능이 느리다.
StringBuilder는 멀티 쓰레드 상황에 안전하지 않지만 동기화 오버헤드가 없으므로 속도가 빠르다.
StringBuffer와 동기화에 관한 내용은 이후에 멀티스레드를 학습해야 이해할 수 있다. 지금은 이런 것이 있구나 정도만 알아두면 된다.
간단한 예제 코드로 메서드 체이닝(Method Chaining)에 대해 알아보자.
package lang.string.chaining;
public class ValueAdder {
private int value;
public ValueAdder add(int addValue) {
value += addValue;
return this;
}
public int getValue() {
return value;
}
}
add() 메서드를 호출할 때 마다 내부의 value 에 값을 누적한다.add() 메서드를 보면 자기 자신( this )의 참조값을 반환한다. 이 부분을 유의해서 보자.package lang.string.chaining;
public class MethodChainingMain1 {
public static void main(String[] args) {
ValueAdder adder = new ValueAdder();
adder.add(1);
adder.add(2);
adder.add(3);
int result = adder.getValue(); System.out.println("result = " + result);
}
}
실행 결과
result = 6
add() 메서드를 여러번 호출해서 값을 누적해서 더하고 출력한다.add() 메서드의 반환값은 사용하지 않았다.이번에는 add() 메서드의 반환값을 사용해보자.
package lang.string.chaining;
public class MethodChainingMain2 {
public static void main(String[] args) {
ValueAdder adder = new ValueAdder();
ValueAdder adder1 = adder.add(1);
ValueAdder adder2 = adder1.add(2);
ValueAdder adder3 = adder2.add(3);
int result = adder3.getValue();
System.out.println("result = " + result);
}
}
실행 결과
result = 6
실행 결과는 기존과 같다.

1. adder.add(1) 을 호출한다.
2. add() 메서드는 결과를 누적하고 자기 자신의 참조값인
this ( x001 )를 반환한다.
3. adder1 변수는 adder 와 같은 x001 인스턴스를 참조한다.

add() 메서드는 자기 자신( this )의 참조값을 반환한다. 이 반환값을 adder1 , adder2 , adder3 에 보관했다.adder , adder1 , adder2 , adder3 은 모두 같은 참조값을 사용한다. 왜냐하면 add() 메서드가 자기 자신( this )의 참조값을 반환했기 때문이다.그런데 이 방식은 처음 방식보다 더 불편하고, 코드도 잘 읽히지 않는다.
이런 방식을 왜 사용하는 것일까?
이번에는 방금 사용했던 방식에서 반환된 참조값을 새로운 변수에 담아서 보관하지 않고, 대신에 바로 메서드 호출에 사용해보자.
package lang.string.chaining;
public class MethodChainingMain3 {
public static void main(String[] args) {
ValueAdder adder = new ValueAdder();
int result = adder.add(1).add(2).add(3).getValue();
System.out.println("result = " + result);
}
}
실행 결과
result = 6
실행 순서
add() 메서드를 호출하면 ValueAdder 인스턴스 자신의 참조값( x001 )이 반환된다. 이 반환된 참조값을 변수에 담아두지 않아도 된다. 대신에 반환된 참조값을 즉시 사용해서 바로 메서드를 호출할 수 있다.
다음과 같은 순서로 실행된다.
adder.add(1).add(2).add(3).getValue() //value=0
x001.add(1).add(2).add(3).getValue() //value=0, x001.add(1)을 호출하면 그 결과로
x001을 반환한다.
x001.add(2).add(3).getValue() //value=1, x001.add(2)을 호출하면 그 결과로 x001을 반환
한다.
x001.add(3).getValue() //value=3, x001.add(3)을 호출하면 그 결과로 x001을 반환한다.
x001.getValue() //value=6
6
메서드 호출의 결과로 자기 자신의 참조값을 반환하면, 반환된 참조값을 사용해서 메서드 호출을 계속 이어갈 수 있다.
코드를 보면 . 을 찍고 메서드를 계속 연결해서 사용한다. 마치 메서드가 체인으로 연결된 것 처럼 보인다. 이러한 기법을 메서드 체이닝이라 한다.
물론 실행 결과도 기존과 동일하다.
기존에는 메서드를 호출할 때 마다 계속 변수명에 . 을 찍어야 했다. 예) adder.add(1) , adder.add(2)
메서드 체이닝 방식은 메서드가 끝나는 시점에 바로 . 을 찍어서 변수명을 생략할 수 있다.
메서드 체이닝이 가능한 이유는 자기 자신의 참조값을 반환하기 때문이다. 이 참조값에 . 을 찍어서 바로 자신의 메서드를 호출할 수 있다.
메서드 체이닝 기법은 코드를 간결하고 읽기 쉽게 만들어준다.
StringBuilder 는 메서드 체이닝 기법을 제공한다.
StringBuilder 의 append() 메서드를 보면 자기 자신의 참조값을 반환한다.
public StringBuilder append(String str) {
super.append(str);
return this;
}
StringBuilder 에서 문자열을 변경하는 대부분의 메서드도 메서드 체이닝 기법을 제공하기 위해 자기 자신을 반환한다.
예) insert() , delete() , reverse()
앞서 StringBuilder 를 사용한 코드는 다음과 같이 개선할 수 있다.
package lang.string.builder;
public class StringBuilderMain1_2 { public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
String string = sb.append("A").append("B").append("C").append("D")
.insert(4, "Java")
.delete(4, 8)
.reverse()
.toString();
System.out.println("string = " + string);
}
}
실행 결과
string = DCBA
정리
"만드는 사람이 수고로우면 쓰는 사람이 편하고, 만드는 사람이 편하면 쓰는 사람이 수고롭다"는 말이 있다.
메서드 체이닝은 구현하는 입장에서는 번거롭지만 사용하는 개발자는 편리해진다.
참고로 자바의 라이브러리와 오픈 소스들은 메서드 체이닝 방식을 종종 사용한다.