F-lab Java 1์ฃผ์ฐจ / Phase 6 / Unit 6.2 ๋ณธ๊ฒฉ ํ์ต ์๋ฃ
9-์น์ ๋ง์คํฐ ํ๋กฌํํธ ํ์์ผ๋ก ๊น์ด ํํค์น๋ค.์ ์ ์ง์: Unit 6.1 (String ๊ณผ Constant Pool)
๋ค์ Unit: 6.3 โ ArrayList vs LinkedList์ด Unit์ ์๋ฏธ: String ์ ๋ถ๋ณ์ฑ์ ๋ณด์ํ๋ ๊ฐ๋ณ ๋ฒํผ.
๋ฉด์ ๋จ๊ณจ (String + vs StringBuilder) + ์ค๋ฌด ํ์ (๋ก๊ทธ, SQL, ๋ณด๊ณ ์).
๋์์ฑ ์ธก๋ฉด์์ ๋์ ์ ํํ ์ฐจ์ด.
String result = "";
for (int i = 0; i < 1000; i++) {
result += "item" + i; // ๋งค๋ฒ ์ ์ข
์ด!
}
// โ 1000์ฅ์ String ๊ฐ์ฒด โ GC ๋ถ๋ด
toString())StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
sb.append("item").append(i); // ๊ฐ์ ๋ณด๋์ ์ถ๊ฐ
}
String result = sb.toString(); // ๋ง์ง๋ง์ ์ข
์ด๋ก
StringBuffer sb = new StringBuffer(); // ๋๊ธฐํ๋จ
// ์ฌ๋ฌ ์ค๋ ๋๊ฐ ์์ ํ๊ฒ ์ฌ์ฉ ๊ฐ๋ฅ
"String ์ ๋ถ๋ณ, StringBuilder/Buffer ๋ ๊ฐ๋ณ. ๋จ์ผ ์ค๋ ๋ = StringBuilder, ๋ฉํฐ ์ค๋ ๋ = StringBuffer (๊ทธ๋ฌ๋ ๊ฑฐ์ ์ ์)."
๋น์ ์ ๋ฆฌ:
| ๋น์ | ์๋ฐ ํด๋์ค | ํน์ฑ |
|---|---|---|
| ์ธ์๋ ์ข ์ด | String | ๋ถ๋ณ, ์์ |
| ๊ฐ์ธ ํ์ดํธ๋ณด๋ | StringBuilder | ๊ฐ๋ณ, ๋น ๋ฆ |
| ๊ณต์ฉ ํ์ดํธ๋ณด๋ | StringBuffer | ๊ฐ๋ณ, ๋๊ธฐํ, ๋๋ฆผ |
์๋ฐ ์ด๊ธฐ (1.0):
๋ฌธ์ ๋ฐ๊ฒฌ:
// ์๋ฐ 1.0 ์๋ ์ฝ๋
String log = "";
for (int i = 0; i < 100000; i++) {
log = log + "[" + i + "] event\n"; // ๋งค๋ฒ ์ String!
}
์ค์ ๋์:
1. log + "[" + i + "] event\n" โ ์ String ์์ฑ
2. ๊ธฐ์กด log ๋ Garbage
3. 100,000 ๋ฒ ๋ฐ๋ณต โ 100,000 ๊ฐ String ์์ฑ โ 99,999 ๊ฐ Garbage
์๊ฐ ๋ณต์ก๋:
์๋ฐ 1.0 ๋ถํฐ StringBuffer ๋ฑ์ฅ:
๊ทธ๋ฌ๋ ํ๊ณ:
synchronizedJava 5 (2004):
StringBuilder sb = new StringBuilder(); // ๋จ์ผ ์ค๋ ๋์ฉ
sb.append("hello");
ํ์ฌ ํ์ค:
Java 5+ ์ปดํ์ผ๋ฌ:
String result = "Hello" + name + ", welcome";
โ ๋ด๋ถ์ ์ผ๋ก ๋ณํ:
String result = new StringBuilder()
.append("Hello")
.append(name)
.append(", welcome")
.toString();
๊ทธ๋ฌ๋ ํ๊ณ โ ๋ฐ๋ณต๋ฌธ์์๋ ์๋ ๋ณํ X:
String result = "";
for (String s : list) {
result = result + s; // ๋งค ๋ฐ๋ณต๋ง๋ค ์ StringBuilder!
}
// โ ์ฌ์ค์ String ๋ง ์ด ๊ฒ๊ณผ ๋์ผํ ๋นํจ์จ
โ ๋ฐ๋ณต๋ฌธ์์๋ ์ง์ StringBuilder ์ฌ์ฉ ํ์.
Java 9 ๋ถํฐ String + ์ ์ปดํ์ผ ๊ฒฐ๊ณผ๊ฐ ๋ ํจ์จ์ :
invokedynamic ์ผ๋ก ๋ฐํ์ ์ต์ ํ๊ทธ๋ฌ๋ โ ๋ฐ๋ณต๋ฌธ์ ์ฌ์ ํ ์ง์ StringBuilder ๊ถ์ฅ.
"๊ฐ๋ณ ๋ฒํผ๋ String ์ ๋ถ๋ณ์ฑ์ ๋ณด์ํ๋ ๋๊ตฌ๋ค."
String ์ ๋ถ๋ณ์ฑ์ ์์ , StringBuilder/Buffer ๋ ํจ์จ. ๋์ด ๊ณต์กดํ๋ ์ด์ .
์๋ฐ๋ ์ฝ๊ธฐ/๊ณต์ ๋ String, ๋ง๋ค๊ธฐ๋ StringBuilder ์ ํจํด ๊ถ์ฅ.
๋จ์ผ ์ค๋ ๋ = StringBuilder, ๋ฉํฐ ์ค๋ ๋ (๊ฑฐ์ ์๋ ์ผ์ด์ค) = StringBuffer.
// โ ๋นํจ์จ ์ฝ๋ โ ์ด์์์ ์์ฃผ ๋ณด์
public String generateReport(List<Cargo> cargos) {
String report = "=== ์ด์ก ๋ณด๊ณ ์ ===\n";
for (Cargo c : cargos) {
report += c.getId() + " | "
+ c.getOrigin() + " โ "
+ c.getDestination() + " | "
+ c.getStatus() + "\n";
}
report += "=== ๋ ===";
return report;
}
ํ์ค ์ํฅ:
ํด๊ฒฐ:
public String generateReport(List<Cargo> cargos) {
StringBuilder sb = new StringBuilder();
sb.append("=== ์ด์ก ๋ณด๊ณ ์ ===\n");
for (Cargo c : cargos) {
sb.append(c.getId()).append(" | ")
.append(c.getOrigin()).append(" โ ")
.append(c.getDestination()).append(" | ")
.append(c.getStatus()).append("\n");
}
sb.append("=== ๋ ===");
return sb.toString();
}
โ ๋ฉ๋ชจ๋ฆฌ โ, ์๋ โ.
// โ ๋นํจ์จ
public String buildQuery(SearchCriteria c) {
String sql = "SELECT * FROM cargos WHERE 1=1";
if (c.getOrigin() != null) {
sql += " AND origin = '" + c.getOrigin() + "'";
}
if (c.getDestination() != null) {
sql += " AND destination = '" + c.getDestination() + "'";
}
if (c.getStatusList() != null) {
for (String status : c.getStatusList()) {
sql += " AND status = '" + status + "'"; // ๋ฐ๋ณต!
}
}
return sql;
}
๋ฌธ์ :
// โ ์ํ: ๋ฉํฐ ์ค๋ ๋์์ StringBuilder
public class LogCollector {
private static StringBuilder log = new StringBuilder();
public static void append(String msg) {
log.append(msg); // โ ๋๊ธฐํ X โ ๋ฐ์ดํฐ ์์ค
}
}
๋ฌธ์ :
ํด๊ฒฐ์ฑ 1: StringBuffer
private static StringBuffer log = new StringBuffer(); // โ ๋๊ธฐํ
ํด๊ฒฐ์ฑ 2: ๋ ์ข์ ๋ฐฉ๋ฒ
// ์ค๋ ๋๋ณ ๋
๋ฆฝ StringBuilder
private static ThreadLocal<StringBuilder> log = ThreadLocal.withInitial(StringBuilder::new);
// ๋๋ ๋์์ฑ ์ปฌ๋ ์
์ฌ์ฉ
private static Queue<String> messages = new ConcurrentLinkedQueue<>();
"String, StringBuilder, StringBuffer ์ ์ฐจ์ด๋?"
๋ชจ๋ฅด๋ฉด:
์๋ฉด:
String + = O(nยฒ) vs StringBuilder = O(n))// ์ผ๊ฒฌ ๋ฌธ์ ์์ด ๋ณด์ด๋ ์ฝ๋
public String formatPhoneNumber(String raw) {
String result = "";
for (char c : raw.toCharArray()) {
if (Character.isDigit(c)) {
result += c; // ์งง์ ๋ฌธ์์ด์ด์ง๋ง...
}
}
return formatHyphens(result);
}
๋์ ์ํฅ:
ํด๊ฒฐ:
public String formatPhoneNumber(String raw) {
StringBuilder sb = new StringBuilder(raw.length());
for (char c : raw.toCharArray()) {
if (Character.isDigit(c)) {
sb.append(c);
}
}
return formatHyphens(sb.toString());
}
// ์ฌ๊ฐํ๊ฒ ๋นํจ์จ
public String buildJson(List<Customer> customers) {
String json = "[";
for (int i = 0; i < customers.size(); i++) {
Customer c = customers.get(i);
json += "{\"id\":" + c.getId()
+ ",\"name\":\"" + c.getName() + "\""
+ ",\"grade\":\"" + c.getGrade() + "\"}";
if (i < customers.size() - 1) {
json += ",";
}
}
json += "]";
return json;
}
๋ ์ข์ ๋ฐฉ๋ฒ:
public String buildJson(List<Customer> customers) {
StringBuilder sb = new StringBuilder("[");
for (int i = 0; i < customers.size(); i++) {
Customer c = customers.get(i);
sb.append("{\"id\":").append(c.getId())
.append(",\"name\":\"").append(c.getName()).append("\"")
.append(",\"grade\":\"").append(c.getGrade()).append("\"}");
if (i < customers.size() - 1) {
sb.append(',');
}
}
sb.append("]");
return sb.toString();
}
| ์๋๋ฆฌ์ค | StringBuilder ๋ชจ๋ฅด๋ฉด | ์๋ฉด |
|---|---|---|
| ๋ณด๊ณ ์ ์์ฑ | ๋ฉ๋ชจ๋ฆฌ ํญ๋ฐ | ํจ์จ์ |
| SQL ๋น๋ฉ | ๋์ GC ๋ถ๋ด | ์์ |
| ๋ฉํฐ ์ค๋ ๋ | race condition | StringBuffer ๋๋ ThreadLocal |
| ๋ฉด์ | ํ๋ฝ | ์๋์ด ๋ต๋ณ |
| ์์ ๋ฉ์๋ ๋์ | ์ ์ฌ ์ฑ๋ฅ ๋ฌธ์ | ์ฒ์๋ถํฐ ์ต์ |
โ ์๋ฐ ์๋์ด์ ๊ธฐ๋ณธ ๋๊ตฌ.
public final class StringBuilder
extends AbstractStringBuilder
implements Serializable, Comparable<StringBuilder>, CharSequence {
// ๋ถ๋ชจ ํด๋์ค AbstractStringBuilder ์ ํ๋
// byte[] value; (Java 9+) ๋๋ char[] (Java 8 ์ดํ)
// int count; ํ์ฌ ๊ธธ์ด
// capacity = value.length
}
ํต์ฌ:
StringBuilder sb = new StringBuilder();
// === ์ถ๊ฐ ===
sb.append("hello") // ๋์ ์ถ๊ฐ
.append(' ')
.append(42) // ์๋ ๋ณํ (int โ "42")
.append(true) // ์๋ ๋ณํ (boolean โ "true")
.append(123.45); // ์๋ ๋ณํ (double โ "123.45")
// === ์ฝ์
===
sb.insert(0, "Start: "); // 0๋ฒ ์์น์ ์ฝ์
// === ์ญ์ ===
sb.delete(0, 7); // 0~7 ์ธ๋ฑ์ค ์ญ์
sb.deleteCharAt(0); // 0๋ฒ ๋ฌธ์ ์ญ์
// === ์นํ ===
sb.replace(0, 5, "World"); // 0~5 ๋ฅผ "World" ๋ก
// === ๋ค์ง๊ธฐ ===
sb.reverse();
// === ์ ๋ณด ===
sb.length(); // ํ์ฌ ๋ฌธ์ ์
sb.capacity(); // ํ์ฌ ๋ฒํผ ํฌ๊ธฐ
sb.charAt(0); // ํน์ ์์น ๋ฌธ์
// === ๋ณํ ===
String result = sb.toString(); // String ์ผ๋ก
String result = new StringBuilder()
.append("Hello")
.append(", ")
.append(name)
.append("!")
.toString();
์ด๊ฒ ๊ฐ๋ฅํ ์ด์ :
this ๋ฐํpublic StringBuilder append(String str) {
super.append(str);
return this; // โ ์๊ธฐ ์์ ๋ฐํ
}
public final class StringBuffer extends AbstractStringBuilder ... {
@Override
public synchronized StringBuffer append(String str) { // โ synchronized!
toStringCache = null;
super.append(str);
return this;
}
// ๊ฑฐ์ ๋ชจ๋ public ๋ฉ์๋๊ฐ synchronized
}
ํต์ฌ:
synchronized โ Thread-Safe| ํด๋์ค | ์๊ฐ | ์ฌ์ฉ ์์ |
|---|---|---|
| String + | ~5์ด | ์ ๋ X |
| StringBuilder | ~5ms | ๋จ์ผ ์ค๋ ๋ (99%) |
| StringBuffer | ~10ms | ๋ฉํฐ ์ค๋ ๋ (๋๋ฌพ) |
| String | StringBuilder | StringBuffer | |
|---|---|---|---|
| ๋ถ๋ณ/๊ฐ๋ณ | ๋ถ๋ณ | ๊ฐ๋ณ | ๊ฐ๋ณ |
| ๋๊ธฐํ | (ํด๋น ์์) | X | โ |
| ์๋ | ๋งค์ฐ ๋๋ฆผ (๋ณ๊ฒฝ ์) | ๋น ๋ฆ | ๋ณดํต |
| Thread-Safe | โ (๋ถ๋ณ) | โ | โ |
| ์๊ฐ ๋ณต์ก๋ | O(nยฒ) | O(n) | O(n) |
| ์ธ์ | ์ฝ๊ธฐ/๊ณต์ | ๋จ์ผ ์ค๋ ๋ ๋น๋ฉ | ๋ฉํฐ ์ค๋ ๋ ๊ณต์ |
| ๋ฑ์ฅ | Java 1.0 | Java 5 | Java 1.0 |
๋ฌธ์์ด์ ์ด๋ป๊ฒ ์ฌ์ฉ?
โโโ ์ฝ๊ธฐ/๊ณต์ ๋ง โ String
โโโ ๋ง๋ค๊ธฐ (๋ฐ๋ณต ๋ฑ)
โ โโโ ๋จ์ผ ์ค๋ ๋ โ StringBuilder โญ
โ โโโ ๋ฉํฐ ์ค๋ ๋ ๊ณต์ (๋๋ฌพ)
โ โโโ StringBuffer
โ โโโ ๋๋ ThreadLocal<StringBuilder>
โโโ ๋น๊ต/์กฐํ โ String + equals
ํ์ค โ 99% ์ ๊ฒฝ์ฐ:
String greeting = "Hello, " + name + "!"; // ์ปดํ์ผ๋ฌ๊ฐ ๋ณํ
Java 8 ๊น์ง:
String greeting = new StringBuilder()
.append("Hello, ")
.append(name)
.append("!")
.toString();
Java 9+ (invokedynamic):
// StringConcatFactory.makeConcatWithConstants() ์ฌ์ฉ
// ๋ ํจ์จ์ , ๋ฐํ์ ์ต์ ํ
๊ทธ๋ฌ๋ ํ๊ณ โ ๋ฐ๋ณต๋ฌธ์ ์๋ ๋ณํ X:
String result = "";
for (String s : list) {
result += s; // ๋งค ๋ฐ๋ณต๋ง๋ค ์ StringBuilder
}
// โ ์ฌ์ค์ O(nยฒ)
โ ๋ฐ๋ณต๋ฌธ์์ ์ง์ StringBuilder ์ฌ์ฉ.
abstract class AbstractStringBuilder {
byte[] value; // ์ค์ ๋ฌธ์ ๋ฐ์ดํฐ (Java 9+, ์ด์ ์ char[])
byte coder; // LATIN1 or UTF16 (Java 9+)
int count; // ํ์ฌ ์ฌ์ฉ ์ค์ธ ๊ธธ์ด
// capacity = value.length / (coder == UTF16 ? 2 : 1)
}
ํต์ฌ ๊ฐ๋ :
count โ ํ์ฌ ๋ฌธ์์ด ๊ธธ์ด (length() ๊ฐ ๋ฐํ)capacity โ ๋ฒํผ ์ ์ฒด ํฌ๊ธฐ (count ๋ณด๋ค ํผ)new StringBuilder() // capacity = 16 (๊ธฐ๋ณธ)
new StringBuilder(100) // capacity = 100
new StringBuilder("hello") // capacity = 5 + 16 = 21
new StringBuilder(initialString) // capacity = initialString.length() + 16
์ 16?:
public void ensureCapacity(int minimumCapacity) {
if (minimumCapacity > value.length) {
int newCapacity = (value.length << 1) + 2; // ํ์ฌ * 2 + 2
if (newCapacity < minimumCapacity) {
newCapacity = minimumCapacity;
}
value = Arrays.copyOf(value, newCapacity);
}
}
ํ์ฅ ๊ท์น:
1. ์ capacity = ํ์ฌ * 2 + 2 (๋ณดํต)
2. ๊ทธ๋๋ ๋ถ์กฑํ๋ฉด = ํ์ํ ํฌ๊ธฐ
์์:
StringBuilder sb = new StringBuilder(); // capacity = 16
sb.append("a".repeat(20)); // 16 โ 34
sb.append("a".repeat(50)); // 34 โ 84
๋น์ฉ:
Arrays.copyOf)์๊ฐ ๋ณต์ก๋:
// โ ํ์ฅ ์ฌ๋ฌ ๋ฒ
StringBuilder sb = new StringBuilder(); // 16
for (int i = 0; i < 1000; i++) {
sb.append("x"); // 16 โ 34 โ 70 โ ... ์ฌ๋ฌ ๋ฒ ํ์ฅ
}
// โ ํ ๋ฒ์ ์ถฉ๋ถํ
StringBuilder sb = new StringBuilder(1024); // 1024
for (int i = 0; i < 1000; i++) {
sb.append("x"); // ํ์ฅ ์์
}
โ ์์ ํฌ๊ธฐ๋ฅผ ์๋ฉด ์ด๊ธฐ capacity ์ง์ .
// StringBuffer ์ ๋ฉ์๋ (๊ฐ๋ตํ)
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
public synchronized String toString() {
if (toStringCache == null) {
return toStringCache = new String(value, 0, count);
}
return new String(toStringCache);
}
lock ๋ฉ์ปค๋์ฆ:
์ฑ๋ฅ ์ํฅ:
public String toString() {
return new String(value, 0, count);
}
ํต์ฌ:
StringBuilder sb = new StringBuilder("hello");
String s = sb.toString();
sb.append(" world");
System.out.println(s); // "hello" (๋ณํ์ง ์์)
System.out.println(sb); // "hello world"
abstract class AbstractStringBuilder {
byte[] value;
byte coder; // LATIN1 (1 byte/char) or UTF16 (2 bytes/char)
}
ํจ๊ณผ:
ILIC ๊ฐ์ ํ๊ธ/์๋ฌธ ํผ์ฉ ์์คํ :
+ vs append ์ ์ปดํ์ผ ๊ฒฐ๊ณผ (Java 9+)// ์์ค ์ฝ๋
String a = "Hello, " + name;
// Java 8 ์ปดํ์ผ ๊ฒฐ๊ณผ (StringBuilder ์ฌ์ฉ)
String a = new StringBuilder()
.append("Hello, ")
.append(name)
.toString();
// Java 9+ ์ปดํ์ผ ๊ฒฐ๊ณผ (invokedynamic)
String a = StringConcatFactory.makeConcatWithConstants(
MethodHandles.lookup(),
"concat",
MethodType.methodType(String.class, String.class),
"Hello, \u0001" // ํจํด
).invoke(name);
โ Java 9+ ๋ ๋ ํจ์จ์ ์ด์ง๋ง, ๋ฐ๋ณต๋ฌธ์์ ์ฌ์ ํ ์ง์ StringBuilder ๊ถ์ฅ.
@Service
public class ReportService {
public String generateMonthlyReport(List<Cargo> cargos) {
// ์์ ํฌ๊ธฐ ๋ฏธ๋ฆฌ ๊ณ์ฐ (์ ํ์ , ๋ ํจ์จ์ )
int expectedSize = cargos.size() * 100; // ํ๊ท 100 ์/์ค
StringBuilder sb = new StringBuilder(expectedSize);
// ํค๋
sb.append("=== ์๊ฐ ์ด์ก ๋ณด๊ณ ์ ===\n")
.append("์ด ํ๋ฌผ ์: ").append(cargos.size()).append("\n")
.append("์์ฑ ์๊ฐ: ").append(LocalDateTime.now()).append("\n\n");
// ๋ณธ๋ฌธ
for (Cargo c : cargos) {
sb.append(String.format("%5d", c.getId()))
.append(" | ").append(c.getOrigin())
.append(" โ ").append(c.getDestination())
.append(" | ").append(c.getStatus())
.append("\n");
}
// ํธํฐ
sb.append("\n=== ๋ ===");
return sb.toString();
}
}
public class CargoQueryBuilder {
public String build(SearchCriteria c) {
StringBuilder sql = new StringBuilder(
"SELECT * FROM cargos WHERE 1=1"
);
List<Object> params = new ArrayList<>();
if (c.getOrigin() != null) {
sql.append(" AND origin = ?");
params.add(c.getOrigin());
}
if (c.getDestination() != null) {
sql.append(" AND destination = ?");
params.add(c.getDestination());
}
if (c.getStatusList() != null && !c.getStatusList().isEmpty()) {
sql.append(" AND status IN (");
for (int i = 0; i < c.getStatusList().size(); i++) {
if (i > 0) sql.append(",");
sql.append("?");
params.add(c.getStatusList().get(i));
}
sql.append(")");
}
return sql.toString();
}
}
๋ณด๋์ค โ ๋ ์์ : PreparedStatement + JOIN ๋ฑ ORM ํ์ฉ ๊ถ์ฅ. ์๋ ํ์ต์ฉ ์์.
public class CsvService {
// โ StringBuilder ๋ก CSV ๋น๋ฉ
public String toCsv(List<Customer> customers) {
StringBuilder sb = new StringBuilder();
// ํค๋
sb.append("ID,Name,Email,Grade\n");
// ๋ฐ์ดํฐ
for (Customer c : customers) {
sb.append(c.getId()).append(',')
.append(escape(c.getName())).append(',')
.append(escape(c.getEmail())).append(',')
.append(c.getGrade())
.append('\n');
}
return sb.toString();
}
// ๊ฐ๋จํ CSV ์ด์ค์ผ์ดํ
private String escape(String value) {
if (value == null) return "";
if (value.contains(",") || value.contains("\"")) {
return "\"" + value.replace("\"", "\"\"") + "\"";
}
return value;
}
}
// โ StringBuilder ์ฌ์ฉ ์ ์ํ
public class UnsafeLogger {
private static final StringBuilder log = new StringBuilder();
public static void append(String msg) {
log.append(msg).append('\n'); // race condition!
}
}
// โ StringBuffer ์ฌ์ฉ
public class SafeLogger {
private static final StringBuffer log = new StringBuffer();
public static void append(String msg) {
log.append(msg).append('\n'); // synchronized
}
}
// โ ๋ ์ข์ ๋ฐฉ๋ฒ: ThreadLocal
public class BetterLogger {
private static final ThreadLocal<StringBuilder> log =
ThreadLocal.withInitial(StringBuilder::new);
public static void append(String msg) {
log.get().append(msg).append('\n'); // ์ค๋ ๋๋ณ ๋
๋ฆฝ
}
public static String getLog() {
return log.get().toString();
}
}
@Benchmark
public class ConcatBenchmark {
private static final int N = 100_000;
@Benchmark
public String stringConcat() {
String result = "";
for (int i = 0; i < N; i++) {
result += "x";
}
return result;
// ๊ฒฐ๊ณผ: ~์ ๋ถ (O(nยฒ))
}
@Benchmark
public String stringBuilder() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < N; i++) {
sb.append("x");
}
return sb.toString();
// ๊ฒฐ๊ณผ: ~5ms (O(n))
}
@Benchmark
public String stringBuilderWithCapacity() {
StringBuilder sb = new StringBuilder(N); // ์ด๊ธฐ capacity
for (int i = 0; i < N; i++) {
sb.append("x");
}
return sb.toString();
// ๊ฒฐ๊ณผ: ~3ms (ํ์ฅ X)
}
@Benchmark
public String stringBuffer() {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < N; i++) {
sb.append("x");
}
return sb.toString();
// ๊ฒฐ๊ณผ: ~10ms (synchronized)
}
@Benchmark
public String streamCollect() {
return IntStream.range(0, N)
.mapToObj(i -> "x")
.collect(Collectors.joining());
// ๊ฒฐ๊ณผ: ~10ms (๋ด๋ถ์ ์ผ๋ก StringBuilder ์ฌ์ฉ)
}
}
@Service
public class NotificationService {
public String buildShipmentNotification(Shipment shipment) {
StringBuilder sb = new StringBuilder();
sb.append("[ILIC ์ด์ก ์๋ฆผ]\n\n");
sb.append("ํ๋ฌผ ๋ฒํธ: ").append(shipment.getId()).append("\n");
sb.append("ํ์ฌ ์ํ: ").append(shipment.getStatus()).append("\n");
if (shipment.getCurrentLocation() != null) {
sb.append("ํ์ฌ ์์น: ").append(shipment.getCurrentLocation()).append("\n");
}
sb.append("์ถ๋ฐ์ง: ").append(shipment.getOrigin()).append("\n");
sb.append("๋์ฐฉ์ง: ").append(shipment.getDestination()).append("\n");
if (shipment.getEstimatedArrival() != null) {
sb.append("์์ ๋์ฐฉ: ")
.append(shipment.getEstimatedArrival().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME))
.append("\n");
}
sb.append("\n์์ธํ ์ ๋ณด๋ ILIC ์ฑ์์ ํ์ธํ์ธ์.");
return sb.toString();
}
}
// ๋ ๊ฐ์ง ๋ฐฉ๋ฒ ๋น๊ต
public class FormattingDemo {
// ๋ฐฉ๋ฒ 1: String.format (๊ฐ๊ฒฐ, ์ฝ๊ฐ ๋๋ฆผ)
public String formatV1(Customer c) {
return String.format("[%d] %s (%s)",
c.getId(), c.getName(), c.getGrade());
}
// ๋ฐฉ๋ฒ 2: StringBuilder (์ฅํฉ, ๋น ๋ฆ)
public String formatV2(Customer c) {
return new StringBuilder()
.append('[').append(c.getId()).append("] ")
.append(c.getName()).append(" (")
.append(c.getGrade()).append(')')
.toString();
}
// ๋ฐฉ๋ฒ 3: + ์ฐ์ฐ (Java 9+ ์์ ๋น์ท)
public String formatV3(Customer c) {
return "[" + c.getId() + "] " + c.getName() + " (" + c.getGrade() + ")";
}
}
์ ํ ๊ธฐ์ค:
+ ๋๋ String.format// โ Stream + Collectors.joining
public String getCustomerNames(List<Customer> customers) {
return customers.stream()
.map(Customer::getName)
.collect(Collectors.joining(", "));
}
// ๊ฒฐ๊ณผ: "Alice, Bob, Charlie"
// ๋ด๋ถ์ ์ผ๋ก StringBuilder ์ฌ์ฉ โ ํจ์จ์
// ๋ ์ ๊ตํ ํํ
public String formatCustomerList(List<Customer> customers) {
return customers.stream()
.map(c -> String.format("[%d] %s", c.getId(), c.getName()))
.collect(Collectors.joining(
"\n", // ๊ตฌ๋ถ์
"=== ๊ณ ๊ฐ ๋ชฉ๋ก ===\n", // ์์
"\n=== ๋ ===" // ๋
));
}
// โ O(nยฒ)
String result = "";
for (String s : list) {
result += s;
}
// โ O(n)
StringBuilder sb = new StringBuilder();
for (String s : list) {
sb.append(s);
}
String result = sb.toString();
// โ ๋ถํ์ํ ๋๊ธฐํ ๋น์ฉ
StringBuffer sb = new StringBuffer();
// ๋จ์ผ ์ค๋ ๋์์๋ง ์ฌ์ฉ
// โ
StringBuilder sb = new StringBuilder();
// โ race condition!
public class Logger {
private static StringBuilder log = new StringBuilder();
public static void log(String msg) {
log.append(msg); // ์ฌ๋ฌ ์ค๋ ๋์์ ํธ์ถ ์ ์ํ!
}
}
// โ StringBuffer ๋๋ ThreadLocal
public class Logger {
private static StringBuffer log = new StringBuffer();
// ๋๋
private static ThreadLocal<StringBuilder> log =
ThreadLocal.withInitial(StringBuilder::new);
}
// โ ํ์ฅ ์ฌ๋ฌ ๋ฒ
StringBuilder sb = new StringBuilder(); // ๊ธฐ๋ณธ 16
for (int i = 0; i < 10000; i++) {
sb.append(largeData); // ์ฌ๋ฌ ๋ฒ ํ์ฅ
}
// โ ์์ ํฌ๊ธฐ ์ง์
StringBuilder sb = new StringBuilder(10000 * 100); // 100์/์ค ๊ฐ์
for (int i = 0; i < 10000; i++) {
sb.append(largeData);
}
StringBuilder sb = new StringBuilder();
// ... ๋น๋ฉ ...
String s1 = sb.toString(); // ์ String ์์ฑ
String s2 = sb.toString(); // ๋ ์ String ์์ฑ!
// โ ํ ๋ฒ๋ง
String result = sb.toString();
String s1 = result;
String s2 = result;
// โ ๊ณผํ ์ฌ์ฉ
public String greet(String name) {
StringBuilder sb = new StringBuilder();
sb.append("Hello, ");
sb.append(name);
sb.append("!");
return sb.toString();
}
// โ ๋จ์ํ ๊ฒฐํฉ์ + ๋ก ์ถฉ๋ถ (Java 9+ ํจ์จ์ )
public String greet(String name) {
return "Hello, " + name + "!";
}
์์น: ๋ฐ๋ณต๋ฌธ์ด ์๋๋ฉด + ๋ OK.
StringBuilder sb1 = new StringBuilder("hello");
StringBuilder sb2 = new StringBuilder("hello");
sb1.equals(sb2); // โ false! (Object.equals โ ์ฐธ์กฐ ๋น๊ต)
์ด์ :
equals() ์ค๋ฒ๋ผ์ด๋ XtoString().equals() ๋๋ compareTo()// โ
sb1.toString().equals(sb2.toString()); // true
// โ StringBuffer ๋ผ๋ ๋ค์์ ์์ X
StringBuffer log = new StringBuffer();
// Thread 1
log.append("event1");
String snapshot = log.toString(); // ๋ค๋ฅธ ์ค๋ ๋๊ฐ ์์ ์ค์ผ ์ ์์
// ์ฆ, ๊ฐ๋ณ ๋ฉ์๋๋ ๋๊ธฐํ๋์ง๋ง ์ฌ๋ฌ ๋ฉ์๋ ๊ฐ์ X
ํด๊ฒฐ: ์ธ๋ถ ๋๊ธฐํ ๋๋ ๋ค๋ฅธ ํจํด.
[Unit 6.1: String + Constant Pool] โ
โ
[Unit 6.2: StringBuilder vs StringBuffer] โ ์ง๊ธ ์ฌ๊ธฐ โ
โ
[Unit 6.3: ArrayList vs LinkedList] (๋ค์)
โ
[Unit 6.4: HashMap ๋ด๋ถ ๊ตฌ์กฐ] โ
โ
โ
โ
[Unit 6.5: TreeMap, LinkedHashMap]
| ํ์ต | ์ฐ๊ฒฐ |
|---|---|
| Unit 4.1 (Heap) | StringBuilder ์ char[] ๋ Heap |
| Unit 5.1 (GC) | String + ์ Garbage ํญ๋ฐ = GC ๋ถ๋ด |
| Unit 6.1 (String) | ๊ฐ๋ณ vs ๋ถ๋ณ์ ๋์กฐ |
// Stream + Collectors.joining ์ ๋ด๋ถ์ ์ผ๋ก StringBuilder ์ฌ์ฉ
list.stream().collect(Collectors.joining(","))
// JDK ์ StringJoiner ๋ ๋ด๋ถ์ StringBuilder
StringJoiner sj = new StringJoiner(", ", "[", "]");
sj.add("a").add("b").add("c");
sj.toString(); // "[a, b, c]"
| ๋ฐฉ์ | ์ค๋ช | ์ ํฉ |
|---|---|---|
| StringBuffer | synchronized ๋ฉ์๋ | ์ ์ฝ๋ |
| ThreadLocal | ์ค๋ ๋๋ณ ๋ ๋ฆฝ | ์ผ๋ฐ์ |
| ConcurrentLinkedQueue + ๋์ค ํฉ์น๊ธฐ | ๋น๋๊ธฐ ์์ง ํ ์ผ๊ด | ๊ณ ์ฑ๋ฅ |
| Reactive (Flux ๋ฑ) | ์คํธ๋ฆผ ๊ธฐ๋ฐ | ์ต์ |
| ์ธ์ด | ๊ฐ๋ณ String |
|---|---|
| Java | StringBuilder, StringBuffer |
| C# | StringBuilder (Java ์ ๋น์ท) |
| Python | list + ''.join() (๊ด์ฉ ํจํด) |
| JavaScript | Array + .join() ๋๋ += (V8 ์ด ์ต์ ํ) |
| C++ | std::string (๊ฐ๋ณ, +=) |
| Rust | String (๊ฐ๋ณ), &str (๋ถ๋ณ) |
| ์ง๋ฌธ | ์ด Unit ์์์ ๋ต |
|---|---|
| String, StringBuilder, StringBuffer ์ฐจ์ด? | ๋ถ๋ณ / ๊ฐ๋ณ / ๋๊ธฐํ |
| ์๊ฐ ๋ณต์ก๋? | String + = O(nยฒ), StringBuilder = O(n) |
| ์ธ์ ์ด๋ค ๊ฑฐ ์ฐ๋? | ๋จ์ผ=Builder, ๋ฉํฐ=Buffer (๋๋ฌพ) |
| capacity ์ length ์ฐจ์ด? | ๋ฒํผ ํฌ๊ธฐ vs ์ค์ ๊ธธ์ด |
| ์๋ ํ์ฅ์? | * 2 + 2 |
1๏ธโฃ String ์ ๋ถ๋ณ, StringBuilder/Buffer ๋ ๊ฐ๋ณ. ์ ์ ์ ํํ ์ฐจ์ด๋ฅผ ์์์ผ.
String = ๋ถ๋ณ, ๋น๊ต/๊ณต์ ์ ์ ํฉ, ๋งค ๋ณ๊ฒฝ ์ ์ ๊ฐ์ฒด (O(nยฒ)). StringBuilder = ๊ฐ๋ณ, ๋จ์ผ ์ค๋ ๋, ๋น ๋ฆ, ๋๊ธฐํ X (Java 5+, ํ์ค ์ ํ). StringBuffer = ๊ฐ๋ณ, ๋ฉํฐ ์ค๋ ๋, synchronized, ๋๋ฆผ (Java 1.0+, ๊ฑฐ์ ์ ์). ๋ชจ๋ ๊ฐ์ ์ธํฐํ์ด์ค (append, insert, delete, replace).
2๏ธโฃ ๋ฐ๋ณต๋ฌธ์์ ๋ฌด์กฐ๊ฑด StringBuilder. ๋จ์ ๊ฒฐํฉ์ + ๋ OK.
๋ฐ๋ณต๋ฌธ์์
String +๋ ๋งค๋ฒ ์ ๊ฐ์ฒด โ O(nยฒ). StringBuilder ๋ ๊ฐ์ ๋ฒํผ์ ์ถ๊ฐ โ O(n). ์ปดํ์ผ๋ฌ๊ฐ ๋จ์ ๊ฒฐํฉ (a + b + c) ์ ์๋ ๋ณํ (Java 9+ invokedynamic ์ผ๋ก ๋ ํจ์จ์ ). ๊ทธ๋ฌ๋ ๋ฐ๋ณต๋ฌธ์ ์๋ ๋ณํ X. ์์ ํฌ๊ธฐ ์๋ฉด ์ด๊ธฐ capacity ์ง์ (ํ์ฅ ๋น์ฉ ์ ์ฝ).3๏ธโฃ ๋จ์ผ ์ค๋ ๋ = StringBuilder, ๋ฉํฐ ์ค๋ ๋ = ThreadLocal ๊ถ์ฅ.
StringBuffer ์ synchronized ๋ ๋จ์ผ ์ค๋ ๋์์๋ ๋น์ฉ. ๋ฉํฐ ์ค๋ ๋์์๋ StringBuffer ๋ณด๋ค ThreadLocal\<StringBuilder> ๋๋ ConcurrentLinkedQueue + ์ผ๊ด ์ฒ๋ฆฌ ๊ฐ ๋ณดํต ๋ ํจ์จ์ . toString() ์ ๋ด๋ถ ๋ฐฐ์ด ๋ณต์ฌ โ ์ String. ILIC ์ ๋ณด๊ณ ์/SQL/JSON ๋น๋ฉ์์ StringBuilder ๊ฐ ์ ๋ต. Stream + Collectors.joining ๋ ๋ด๋ถ์ StringBuilder.
Q1: String result = result + "x"; ๋ฅผ 1๋ง ๋ฒ ๋ฐ๋ณตํ๋ฉด ์ ๋นํจ์จ์ ์ธ์ง ๋ฉ๋ชจ๋ฆฌ ๊ด์ ์์ ์ค๋ช
ํ๋ผ.
ํ ์ค ๋ต: ๋งค๋ฒ ์ String ๊ฐ์ฒด ์์ฑ + ๊ธฐ์กด ๊ฐ์ฒด ๋ณต์ฌ โ O(nยฒ) + GC ๋ถ๋ด.
์์ธ ์ค๋ช :
String result = ""; // ๋น String ๊ฐ์ฒด #1
result = result + "x"; // ์ด๋ป๊ฒ ๋์?
์ค์ ๋์:
1. result + "x" ์คํ
2. ์ปดํ์ผ๋ฌ๊ฐ โ new StringBuilder(result).append("x").toString() ์ผ๋ก ๋ณํ
3. โ ์ String ๊ฐ์ฒด #2 ("x") ์์ฑ
4. result ๊ฐ #2 ๋ฅผ ๊ฐ๋ฆฌํด
5. ๊ฐ์ฒด #1 ("") ์ Garbage
๋ฉ๋ชจ๋ฆฌ ์ํ:
[Heap]
โโโ String #1 "" (Garbage)
โโโ String #2 "x" โ result
result = result + "x"; // result ๋ 99์ String
๋์:
1. result (99์) + "x" โ ์ String 100์
2. 99์ String ์ Garbage
3. ๋งค๋ฒ n ๊ธ์ ๋ณต์ฌ ๋ฐ์
์๊ฐ:
result = result + "x"; // result ๋ 9999์
๋์:
์ด ์ฒ๋ฆฌ ๊ธ์ ์:
1 + 2 + 3 + ... + 10000
= 10000 * 10001 / 2
= 50,005,000 (์ฝ 5์ฒ๋ง)
์ด ์์ฑ ๊ฐ์ฒด ์:
10000 ๊ฐ String ๊ฐ์ฒด
9999 ๊ฐ๊ฐ Garbage
๋ฉ๋ชจ๋ฆฌ ์๋น:
T(n) = 1 + 2 + 3 + ... + n
= n(n+1)/2
= O(nยฒ)
์ค์ธก (1๋ง ํ):
String + ๋ฐ๋ณต:
๋ฐ๋ณต 1: [""] โ [..."x"] (์ ๊ฐ์ฒด)
๋ฐ๋ณต 2: ["x"] โ [..."xx"] (์ ๊ฐ์ฒด, 1์ ๋ณต์ฌ)
๋ฐ๋ณต 3: ["xx"] โ [..."xxx"] (์ ๊ฐ์ฒด, 2์ ๋ณต์ฌ)
...
๋ฐ๋ณต N: ["xxx...x"] โ [..."xxx...xx"] (์ ๊ฐ์ฒด, N-1 ์ ๋ณต์ฌ)
โ N ๊ฐ ๊ฐ์ฒด + ๋์ Nยฒ/2 ๊ธ์ ๋ณต์ฌ
StringBuilder:
์ด๊ธฐ: [๋ฒํผ 16์, ์ฌ์ฉ 0]
๋ฐ๋ณต 1: [๋ฒํผ 16์, ์ฌ์ฉ 1] (์๋ฆฌ ์ถ๊ฐ)
๋ฐ๋ณต 2: [๋ฒํผ 16์, ์ฌ์ฉ 2]
...
๋ฐ๋ณต 16: [๋ฒํผ 16์, ์ฌ์ฉ 16] (๊ฝ ์ฐธ)
๋ฐ๋ณต 17: [๋ฒํผ 34์, ์ฌ์ฉ 17] (ํ์ฅ!)
...
๋ฐ๋ณต N: [์ถฉ๋ถํ ๋ฒํผ, ์ฌ์ฉ N]
โ 1 ๊ฐ ๊ฐ์ฒด + ๊ฐ๋ ํ์ฅ โ ๋์ ~2N ๊ธ์ (๋ถํ ์ํ O(n))
String ์ ๋ถ๋ณ์ฑ ์ ์์ ์ด๋ผ๋ ์ด์ ์ ์ฃผ์ง๋ง,
๋ฐ๋ณต์ ๋ณ๊ฒฝ ์๋ ์น๋ช ์ ๋นํจ์จ.
StringBuilder ๊ฐ ๊ฐ์ ์์ ์ 1000๋ฐฐ ๋น ๋ฅด๊ฒ.
โ ์๋ฐ์์ StringBuilder ๊ฐ ๋จ์ํ ๋๊ตฌ๊ฐ ์๋๋ผ ๊ธฐ๋ณธ ๋๊ตฌ.
Q2: StringBuilder ์ capacity ์ ์๋ ํ์ฅ์ ๋น์ฉ์?
ํ ์ค ๋ต: capacity ๋ถ์กฑ ์ ํ์ฌ * 2 + 2 ๋ก ํ์ฅ + ๊ธฐ์กด ๋ฐ์ดํฐ ๋ณต์ฌ. ๋ถํ ์ํ O(n).
์์ธ ์ค๋ช :
StringBuilder sb = new StringBuilder();
System.out.println(sb.capacity()); // 16
System.out.println(sb.length()); // 0
sb.append("hello");
System.out.println(sb.capacity()); // 16 (ํ์ฅ ์์)
System.out.println(sb.length()); // 5
๊ฐ๋ :
capacity = ๋ด๋ถ ๋ฒํผ ํฌ๊ธฐ (value.length)length = ์ค์ ์ฌ์ฉ ์ค์ธ ๋ฌธ์ ์ (count)length <= capacity ํญ์ ์ฑ๋ฆฝ๊ทธ๋ฆผ:
[StringBuilder ๋ด๋ถ]
โโโ byte[] value: [h][e][l][l][o][_][_][_][_][_][_][_][_][_][_][_]
โ 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
โ โ โ
โ length=5 capacity=16
public void ensureCapacityInternal(int minimumCapacity) {
if (minimumCapacity - value.length > 0) {
value = Arrays.copyOf(value, newCapacity(minimumCapacity));
}
}
private int newCapacity(int minCapacity) {
int newCapacity = (value.length << 1) + 2; // ํ์ฌ * 2 + 2
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity;
}
return newCapacity;
}
ํ์ฅ ๊ท์น:
1. ์ capacity = ํ์ฌ * 2 + 2
2. ๊ทธ๋๋ ๋ถ์กฑ โ ํ์ํ ํฌ๊ธฐ๋ก
์์:
์ด๊ธฐ: capacity = 16
์ฒซ ํ์ฅ: 16 โ 34 (16 * 2 + 2)
๋ ๋ฒ์งธ: 34 โ 70
์ธ ๋ฒ์งธ: 70 โ 142
๋ค ๋ฒ์งธ: 142 โ 286
...
๋น์ฉ:
1. ์ ๋ฐฐ์ด ํ ๋น โ O(์ ํฌ๊ธฐ)
2. ๊ธฐ์กด ๋ฐ์ดํฐ ๋ณต์ฌ โ Arrays.copyOf โ O(๊ธฐ์กด ํฌ๊ธฐ)
3. ์ ๋ฐฐ์ด์ GC ๋์
์๊ฐ:
N ๊ฐ ๋ฌธ์ ์ถ๊ฐ:
- ํ์ฅ ํ์: logโ(N) (๋๋ต)
- ๊ฐ ํ์ฅ์ ๋น์ฉ: O(ํ์ฌ ํฌ๊ธฐ)
- ๋์ ๋ณต์ฌ: 16 + 34 + 70 + 142 + ... โ 2N
์ด ๋น์ฉ: N (์ถ๊ฐ) + 2N (๋ณต์ฌ) = O(n)
โ append ํ ๋ฒ = O(1) ํ๊ท (๋ถํ ์ํ).
๋์ ์ โ ํ์ฅ ๋น๋ฒ:
StringBuilder sb = new StringBuilder(); // 16
for (int i = 0; i < 1_000_000; i++) {
sb.append("x");
}
// ํ์ฅ: 16 โ 34 โ 70 โ 142 โ ... โ 1,048,594
// ์ฝ 17 ๋ฒ ํ์ฅ
// ๋์ ๋ณต์ฌ: ~ 2 ๋ฐฑ๋ง ๊ธ์
์ข์ ์ โ ํ ๋ฒ์ ์ถฉ๋ถํ:
StringBuilder sb = new StringBuilder(1_000_000); // 100๋ง
for (int i = 0; i < 1_000_000; i++) {
sb.append("x");
}
// ํ์ฅ: 0 ๋ฒ
// ๋์ ๋ณต์ฌ: 0
// โ ์ฝ 30% ๋น ๋ฆ
| ์ํฉ | ๊ถ์ฅ |
|---|---|
| ์งง์ ๊ฒฐํฉ (~100 ์) | ๊ธฐ๋ณธ (16) OK |
| ์ค๊ฐ (~1KB) | ๋ช ์ (1024) |
| ํฐ (~MB) | ๋ช ์ (์์ ํฌ๊ธฐ) |
| ์ ํํ ์์ธก ์ด๋ ค์ | ๊ธฐ๋ณธ ์ฌ์ฉ (์๋ ํ์ฅ OK) |
capacity ์ length ๋ ๋ค๋ฅด๋ค โ capacity ๋ ๋ฒํผ, length ๋ ์ค์ .
์๋ ํ์ฅ์ ํจ์จ์ (๋ถํ ์ํ O(1)) ์ด์ง๋ง, ์์ ํฌ๊ธฐ๋ฅผ ์๋ฉด ์ด๊ธฐ capacity ์ง์ ์ผ๋ก ๋ ๋์.
1๋ง ์ String ๋น๋ฉ ์ โ capacity ์ง์ ์ผ๋ก ์ฝ 30% ์ฑ๋ฅ ๊ฐ์ .