StringBuilder in Java
Introduction
Java에서 사용하는 String 클래스는 Immutable 하기 때문에, 아래와 같이 Concatenate와 같이 문자열이 변경되는 경우, 새로운 String 클래스를 정의하여 할당된다. 따라서 Heap영역에 a, b를 할당 받고, a+b를 할당받는 시점에서 ab를 위한 새로운 String 객체가 생성되어 Garbage Collector에 의해 Free되기 전까지 메모리를 차지하게 된다.
String a = "A";
String b = "B";
String ab = a+b;
이러한 Immutable의 단점을 해결하기 위해 StringBuffer(thread-safe) 및 StringBuilder(thread-unsafe)를 사용하며, 이를 사용하기 위해서는 다른 문제점을 같이 생각해야 한다.
Details
StringBuilder는 간단하게 append() 함수를 이용하여 String을 Concatenation 할 수 있으며 Time-Complexity와 Space-Complexity에서 많은 이점을 가진다. Java Compiler에서는 String의 + 연산자를 이용해서 연결하더라도 StringBuilder로 컴파일 해 주기도 한다.
Initialize
StringBuilder를 사용할 때 대부분의 개발자는 new StringBuilder()를 그대로 사용하는데, 만약 연결하고자 하는 데이터의 크기를 가늠할 수 있다면 초기화 시 매개변수로 넣어 불필요한 연산을 줄여야 한다. 만약 매개변수 없이 초기화 할 경우 INITIAL_SIZE(16)으로 할당되며, new StringBuilder(String)으로 초기화 할 경우 (String.length + INITIAL_SIZE)로 초기화를 수행한다.
Append
StringBuilder의 append() 메소드를 사용하면 아래와 같이 상황에 따라 다르게 동작한다.
- String을 append할 capacity가 있을 때
- 매개변수로 입력받은 String을 빈 공간에 할당
- String을 append할 capacity가 없을 때(DoS 위험 존재)
- max(2*capacity, 기존크기+입력받은 크기)로 char[] 배열을 다시 생성
- 생성된 새로운 배열에 값을 할당
Vulnerable Case
StringBuilder의 취약점은 Append의 2-a에서 입력받은 크기의 검사가 없어 매우 긴 입력값이 들어오거나 반복해서 공간을 확장해야 하는 경우, Garbage Collector에 의해 메모리가 수거되기 전까지 남아있어 Heap 공간이 가득 찰 수 있다. 이러한 문제 때문에 외부에서 입력받는 값을 append 할 때에는 입력 값의 최대 길이를 제한해야 한다.
Good Practice
사용자에게 입력되는 값이 고정 길이인 경우, 해당 길이가 일치하는지 확인하며 StringBuilder 초기화 시 예상하는 길이를 입력하여 성능을 최대화 한다. 사용자에게 입력되는 값이 가변 길이인 경우, 반드시 적당한 최대 길이(여기에서는 MAX_LENGTH)보다 길이가 짧은 경우에만 append 하도록 해야한다. StringBuilder 초기화 시 적당한(예제에서는 MIN_LENGTH)값을 입력하는 것은 성능을 위한 권장 사항이다.
package io.github.bindon.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
public class StringBuilderController {
private static final int MIN_LENGTH = 256;
private static final int MAX_LENGTH = 32768;
@RequestMapping("/appendForFixed")
public String appendForFixed(@RequestParam String original, @RequestParam String[] elements) throws Exception {
StringBuilder stringBuilder = new StringBuilder(elements.length * MAX_LENGTH);
for(String element: elements) {
if(element.length() == MAX_LENGTH) {
stringBuilder.append(element);
} else {
throw new Exception("Invalid Length");
}
}
return stringBuilder.toString();
}
@RequestMapping("/appendForDynamic")
public String appendForDynamic(@RequestParam String original, @RequestParam String[] elements) throws Exception {
StringBuilder stringBuilder = new StringBuilder(elements.length * MIN_LENGTH);
for(String element: elements) {
if(stringBuilder.length() + element.length() < MAX_LENGTH) {
stringBuilder.append(element);
} else {
throw new Exception("Invalid Length");
}
}
return stringBuilder.toString();
}
}
Secure Coding Post List
TITLE | DATE | StringBuilder in Java | 2020-02-26 | HTTP Response Splitting | 2020-02-26 | Array Assignment in Java | 2020-02-26 |
---|