
코테 풀다가 한 번쯤 턱 맞는 포인트
프로그래머스 같은 데서 이런 문제 한 번쯤 본 적 있을 거다.
문자열
my_string과 정수k가 주어질 때,my_string을k번 반복한 문자열을 return 하세요.
대부분 처음에는 이렇게 짠다.
public String solution(String my_string, int k) {
String answer = "";
for (int i = 0; i < k; i++) {
answer += my_string;
}
return answer;
}
테스트 몇 개 돌려보면 잘 동작하고, 길이도 작으면 제출해도 그냥 통과한다.
근데 문자열 길이랑 k가 좀만 커지면 이런 코드 패턴이 진짜 성능 구멍이 된다.
이유는 하나다.
Java 의 String 은 “불변(immutable)” 이기 때문.
이 글에서는
String 이 불변이라는 게 정확히 무슨 뜻인지
왜 그렇게 설계됐는지
코테/실무에서 어디서 사고가 나는지
그래서 실제로 코드를 어떻게 짜는 게 좋은지
정리해보려고 한다.
1.String 이 불변이라는 말의 의미
아주 단순한 코드 하나 보자.
String s = "hello";
s.toUpperCase();
System.out.println(s); // hello
처음 보면 여기서 한 번씩 당황한다.
“대문자로 바꾸라고 했는데 왜 그대로지?”
이유는 간단하다.
toUpperCase() 는 기존 String 을 수정하는 메서드가 아니다.
대문자로 변환된 새 String 객체 를 만들어서 리턴할 뿐이다.
원래 "hello" 라는 문자열 객체는 그대로 남아 있다.
정상적인 사용은 이렇게 된다.
String s = "hello";
s = s.toUpperCase();
System.out.println(s); // HELLO
정리하면:
한 번 만들어진 String 객체의 내용은 절대 안 바뀐다.
바뀌는 건 그 객체를 가리키는 변수 뿐이다.
이게 String 이 불변(immutable) 이라는 말의 의미다.
2. 왜 굳이 String 을 불변으로 만들었을까?
“그냥 가변으로 만들어서 바꾸기 편하게 해주지…” 싶은데,
Java 쪽에서 일부러 불변으로 설계한 이유가 몇 가지 있다.
2-1. 보안(Security)
String 은 이런 데 자주 쓰인다.
DB 커넥션 URL, 사용자 ID, Password
파일 경로
Class 이름, 리플렉션 관련 문자열
네트워크 주소, 각종 설정값
만약 String 이 가변이면,
어딘가에서 참조를 들고 있다가 중간에 값을 바꿔버리는 것도 가능해진다.
String password = "secret";
// 다른 코드가 password 를 바꿔버릴 수 있다면?
불변이면 이런 류의 공격 자체가 구조적으로 막힌다.
한 번 만들어진 문자열의 내용은 누구도 바꿀 수 없기 때문.
2-2. 스레드 안전(Thread-Safety)
멀티스레드 환경에서 여러 스레드가 같은 문자열을 공유하는 경우가 많다.
같은 URL
같은 SQL 쿼리
같은 메시지 템플릿 등
만약 가변 타입이면, 동기화 안 하면 레이스 컨디션, 데이터 깨짐이 발생할 수 있다.
하지만 String 은 불변이라서,
여러 스레드가 동시에 읽기만 해도 안전 하고
따로 synchronized 를 걸 필요도 없다.
그래서 JVM 내부, 라이브러리 내부에서도 String 을 마음 편하게 공유해서 쓴다.
2-3. String Pool & 캐싱
Java 에는 String Pool(상수 풀) 이라는 개념이 있다.
String a = "hello";
String b = "hello";
System.out.println(a == b); // true (같은 객체)
리터럴 "hello" 는 JVM 상수 풀에 한 번만 올라가고,
같은 리터럴을 쓰는 변수는 그 객체를 재사용한다.
이게 가능한 이유도 String 이 불변이기 때문이다.
누군가 "hello" 라는 문자열의 내용을 중간에 바꿔버릴 수 있다면,
상수 풀이라는 개념이 성립할 수 없다.
추가로 String 은 hashCode() 결과도 한 번 계산해두고 재사용한다.
그래서 HashMap 의 key, HashSet 의 원소로 자주 쓰이는 것도 이 덕분이다.
3.불변이라서 생기는 성능 문제 – 코테 예시로 보기
다시 처음 코테 문제로 돌아가 보자.
my_string 을 k번 반복해서 이어붙인 문자열을 반환
처음 짰던 코드:
public String solution(String my_string, int k) {
String answer = "";
for (int i = 0; i < k; i++) {
answer += my_string;
}
return answer;
}
겉으로 보면
첫 번째 반복: "" + "abc" → "abc"
두 번째 반복: "abc" + "abc" → "abcabc"
세 번째 반복: "abcabc" + "abc" → "abcabcabc"
이렇게 보이는데, 내부에서는 이런 일이 일어난다.
answer = answer + my_string;
이 한 줄이 컴파일되면 실제로는 대략:
answer = new StringBuilder(answer)
.append(my_string)
.toString();
이런 식으로 바뀐다.
즉, 매 반복마다:
기존 answer 내용을 전부 복사해서
my_string 을 뒤에 붙인 새 문자열을 만들고
그걸 다시 answer 에 대입
길이가 커질수록 매번 더 긴 문자열을 계속 복사하는 구조라서
결국 전체 시간 복잡도는 O(n²) 쪽으로 간다.
문자열이 10개, 100개 이럴 때는 티가 안 나는데,
길이 10만, 20만 이런 쪽으로 가면 이 차이가 바로 시간 초과로 튀어나온다.
4.StringBuilder 를 써야 하는 이유
그래서 문자열 누적할 때는 보통 이렇게 바꾼다.
public String solution(String my_string, int k) {
StringBuilder sb = new StringBuilder(my_string.length() * k);
for (int i = 0; i < k; i++) {
sb.append(my_string);
}
return sb.toString();
}
StringBuilder 는 내부에 char[] 버퍼를 하나 들고 있고,
append() 할 때마다 그 버퍼에 글자를 채워 넣는 식으로 동작한다.
버퍼가 부족해지면 가끔씩만 크게 확장
전체적으로는 O(n) 에 가까운 시간 복잡도
여기서
new StringBuilder(my_string.length() * k);
이렇게 capacity 를 미리 잡아두는 이유는,
최종 문자열 길이를 대략 알고 있으니까
중간에 버퍼 확장(reallocate) 이 거의 안 일어나게 하려는 것
코테에서 “문자열 이어붙이기” 계열 문제는
이 패턴 하나만 붙잡고 있어도 웬만한 건 다 처리된다.
5.“컴파일러가 + 를 StringBuilder 로 바꿔준다면서?”
자바 컴파일러가 문자열 더하기를 최적화해 준다는 얘기도 많이 들을 거다.
String s = "a" + "b" + "c";
이 코드는 컴파일할 때 대략 이렇게 바뀐다.
String s = new StringBuilder()
.append("a")
.append("b")
.append("c")
.toString();
그래서 한 줄 안에서 상수 + 상수 + 상수 이런 건 크게 걱정 안 해도 된다.
문제는 반복문 안에서 누적할 때 이다.
String s = "";
for (int i = 0; i < n; i++) {
s = s + "a";
}
이건 반복마다 다음처럼 바뀐다.
s = new StringBuilder(s)
.append("a")
.toString();
매번 새로운 StringBuilder 를 만들고,
이전 문자열 전체를 복사하고, 새 String 을 만들고… 이게 반복된다.
그래서 실질적인 가이드는 이렇게 보면 된다.
한 줄 안에서 "a" + "b" + "c"
→ 컴파일러가 알아서 최적화해주니 괜찮다.
반복문 안에서 문자열 누적 (+=, s = s + ...)
→ 성능 구멍이 될 수 있으니 무조건 StringBuilder 를 쓰는 게 안전하다.
6.Java 11 이상이라면 repeat() 도 있다
문제 조건이 “문자열을 k번 반복해서 이어붙여라” 딱 이 정도면,
Java 11 이상에서는 아예 메서드가 준비돼 있다.
public String solution(String my_string, int k) {
return my_string.repeat(k);
}
내부 구현도 결국 StringBuilder 비슷한 방식으로 효율적으로 동작하기 때문에,
환경이 허용하면 이게 제일 깔끔하다.
정리
Java 11+ : my_string.repeat(k);
Java 8 기준 : StringBuilder + append 반복
7.불변이라서 생기는 또 다른 함정들
불변이라는 특성 때문에 자주 헷갈리는 부분 몇 개만 정리해 둔다.
7-1. 문자열 메서드들은 전부 “새 String 반환”
아래 코드를 보자.
String s = " hello ";
s.trim();
s.toUpperCase();
s.replace("H", "J");
System.out.println(s); // " hello "
값이 하나도 안 바뀐다.
이유는:
trim(), toUpperCase(), replace() 전부
기존 문자열을 고치는 게 아니라, 새 문자열을 만들어서 리턴하는 메서드이기 때문
정상적인 사용은 이렇게 해야 한다.
String s = " hello ";
s = s.trim(); // "hello"
s = s.toUpperCase(); // "HELLO"
s = s.replace("H", "J"); // "JELLO"
체이닝해도 마찬가지다.
String s = " hello ";
s = s.trim()
.toUpperCase()
.replace("H", "J");
7-2. substring 도 동일
String s = "abcdef";
String sub = s.substring(0, 3); // "abc"
System.out.println(s); // "abcdef"
System.out.println(sub); // "abc"
substring() 이 원본을 자르는 게 아니라,
잘라진 부분만 따로 담은 새 String 을 만들어준다고 생각하면 된다.
8.코테/실무에서 String 불변 때문에 생기는 패턴 정리
마지막으로, 실전에서 그냥 규칙처럼 가져가도 되는 패턴들만 정리해본다.
반복문에서 문자열 누적 → 무조건 StringBuilder
StringBuilder sb = new StringBuilder();
for (...) {
sb.append(...);
}
String result = sb.toString();
출력 많이 하는 백준/프로그래머스 스타일 문제
StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; i++) {
// 로직
sb.append(result).append('\\n');
}
System.out.print(sb);
문자열 반복 (my_string, k 문제 같은 것)
Java 11 이상: my_string.repeat(k)
Java 8 기준: StringBuilder 로 직접 반복
문자열 조작 메서드들은 전부 “새 객체 반환”이라는 걸 항상 기억하기
trim, toUpperCase, toLowerCase, replace, substring …
호출만 하고 변수에 다시 대입 안 하면, 원본은 그대로다.
9.마무리
정리하면, 머릿속에는 이 정도만 남겨두면 된다.
String 은 한 번 만들어지면 안 바뀐다.
바뀌는 건 그걸 가리키는 변수뿐이다.
여기에:
반복문에서 문자열 누적 시 StringBuilder 써라
많이 반복되는 문자열은 capacity 미리 잡아라
문자열 메서드는 항상 “새 String” 을 리턴한다
이 정도만 붙여두면,
코테에서도 시간초과 덜 나고 실무 코드에서도 의미 없이 느린 문자열 처리 코드를 피할 수 있다.
나중에 기회 되면 여기서 파생되는 주제들
(예: String Pool, intern(), == vs equals() 비교 등)
따로 빼서 면접 대비용으로 정리해도 괜찮을 것 같다.
::contentReference\[oaicite:0\]{index=0}'Develop > Back-End' 카테고리의 다른 글
| [Java] 리플렉션(Reflection) 이란 무엇인가요? (7) | 2024.10.07 |
|---|---|
| [SpringBoot] 스프링을 이용하여 가벼운(간단한) 스케줄링 작업 처리하는 방법 😁 (4) | 2024.08.13 |
| [Java] 모던 자바(Modern JAVA) 란 무엇인가!!!!😒 (feat. 새롭게 추가된 기능들) (105) | 2024.03.24 |
| 스프링 배치(Spring Batch) 시작하기 !😭 (131) | 2024.02.03 |
| 스프링 부트 카카오 로그인 API 기능 추가하기 😳 (114) | 2023.10.22 |