J-S-10 불변 객체의 원리와 String 클래스 완벽 해부
2026-01-02 |
글 정보
- 카테고리
- Programming/Java/Starter
- 태그
- JavaLevel2
1. 불변 객체(Immutable Object)의 논리적 개념
불변 객체란?
- '읽기 전용' 클래스를 의미합니다.
- 객체 생성 후에는 내부의 상태를 절대 변경할 수 없습니다.
왜 '불변'이어야 할까요? (Side Effect 방지)
- 참조(Reference)의 특성 때문입니다.
- 특정 인스턴스를 가리키는 참조 변수는 여러 개가 될 수 있습니다.
- 이를 프로그래밍적으로 막을 방법은 없습니다.
- 매개변수 전달, 멀티스레딩 환경에서 흔히 발생합니다.
- 만약 객체가 가변(Mutable)이라면?
- 모든 참조자가 해당 인스턴스에 대해 읽기(Read)와 쓰기(Write) 권한을 가집니다.
- 누군가 값을 변경하면, 이를 참조하는 다른 곳에서도 의도치 않게 값이 변합니다.
- 결과: 데이터의 무결성이 깨지는 심각한 논리적 버그가 발생합니다.
2. 불변 객체의 정의와 설계
어떻게 구현하나요?
- 모든 필드를
final로 선언하여 상수화합니다.
- 생성자에서 필드 초깃값을 설정한 뒤에는 변경할 수 없게 만듭니다.
값을 변경해야 할 때는요?
- 값을 수정하는 대신, 수정된 값을 가진 '새로운 객체'를 반환합니다.
- Copy & Write 방식입니다.
- 예:
replace(), concat() 등
- 장점:
- 다소 비효율적으로 보일 수 있으나 원자성을 보장합니다.
- 멀티스레딩 환경에서 동기화 없이도 안전하게 공유할 수 있습니다.
네이밍 컨벤션 (with~)
- 불변 객체에서 상태가 변경된 새 복사본을 반환할 때 주로 사용됩니다.
- 예:
withName(String name)
- "이름만 변경된(with name) 새로운 객체를 줘"라는 의미를 내포합니다.
- (Setter와 구분하기 위한 최신 불변 객체 패턴 중 하나입니다.)
대표적인 불변 객체
- String
- Wrapper Class (Integer, Long, Double 등)
코드예제
public class UserSettings {
// 1. 모든 필드를 final로 선언하여 한 번 값이 정해지면 바꿀 수 없게 합니다.
private final String theme;
private final int volume;
// 2. 생성자를 통해 최초 상태를 설정합니다.
public UserSettings(String theme, int volume) {
this.theme = theme;
this.volume = volume;
}
// 3. 값 변경이 필요할 때: Setter 대신 '새로운 객체'를 반환하는 메서드(with~)를 만듭니다.
public UserSettings withTheme(String newTheme) {
// 기존 volume은 유지하고, 변경된 theme을 가진 새 객체를 생성하여 반환 (Copy & Write)
return new UserSettings(newTheme, this.volume);
}
// 단순 확인을 위한 Getter (Setter는 없음)
public String getTheme() { return theme; }
}
3. 문자열 상수와 String 클래스
문자열(String)의 본질
- 본질은 문자 배열(Array)입니다.
- 따라서 인코딩 규칙과 배열 길이의 제약을 받습니다.
- 문자열은 이론적으로 가변 길이 데이터입니다.
- 하지만 배열 기반이므로 최대 길이 제한(Heap 메모리 한계)이 존재합니다.
내부 구조의 변화 (Java 9)
- Java 8 이전:
char[] (문자당 2byte)
- Java 9 이후:
byte[] + coder (Compact Strings)
- 영어만 쓸 때는 1byte로 압축하여 메모리 효율을 높였습니다.
String의 이중성
- 참조형(Reference Type)이지만 기본형(Primitive Type)처럼 쓰입니다.
- 리터럴 표기(
" ")가 가능합니다.
- 불변(Immutable) 특성을 가집니다.
- 덧셈(
+) 연산 시 원본이 바뀌는 게 아니라, 매번 새로운 임시 객체가 생성됩니다.
- 주의: 반복문 안에서 문자열 덧셈을 하면 메모리 효율이 극도로 떨어집니다.
StringBuilder 사용을 권장합니다.

4. 문자열의 비교 (핵심)
상등 연산자 (==) 사용 금지
- 두 String 인스턴스의 주소값(메모리 위치)을 비교합니다.
- 문자열 상수 풀(String Constant Pool)
- 리터럴(
"")로 생성 시 JVM이 미리 만들어둔 풀에서 가져옵니다.
new String("")으로 생성 시 힙 영역에 새로 만듭니다.
- 생성 방식에 따라 주소가 다르므로, 내용이 같아도
false가 나올 수 있습니다.
올바른 비교 방법
equals()
- 가장 보편적이고 추상화된 값 비교 메서드입니다.
- 효율성: 문자열의 길이(Length)를 먼저 체크하고, 다르면 바로
false를 반환하므로 빠릅니다.
compareTo()
- 두 문자열의 사전적 순서를 비교하여 정수(
int)를 반환합니다.
- 명확성: 단순 같다/다르다가 아니라 '앞선다/뒤선다'를 명확히 판별합니다.
- 단점: 순서를 알아내기 위해 전체를 순회해야 하므로
equals보다 느릴 수 있습니다.
코드 예시
public class StringComparison {
public static void main(String[] args) {
// 리터럴 방식: 상수 풀(Pool) 이용
String poolStr = "Java";
// new 방식: 힙(Heap) 메모리에 강제로 새 객체 생성
String heapStr = new String("Java");
// 1. 주소 비교 (==)
// 내용은 같지만 메모리 위치가 달라서 false가 나옵니다.
System.out.println(poolStr == heapStr); // false
// 2. 값 비교 (equals)
// 주소와 상관없이 문자열의 내용(Value)만 비교하므로 true입니다.
System.out.println(poolStr.equals(heapStr)); // true
// 3. 사전 순서 비교 (compareTo)
// 두 문자열이 같으면 0, 다르면 사전 순서 차이를 반환합니다.
System.out.println(poolStr.compareTo(heapStr)); // 0
}
}
5. String 클래스 주요 메서드
문자 추출 및 검색
char charAt(int index): 특정 위치의 문자 반환
int indexOf(String str): 특정 문자열이 시작되는 인덱스 반환
boolean contains(CharSequence s): 포함 여부 확인
boolean startsWith(String prefix): 시작 문자열 확인
boolean endsWith(String suffix): 끝 문자열 확인
비교
boolean equals(Object anObject): 내용 비교
boolean equalsIgnoreCase(String another): 대소문자 무시하고 비교
int compareTo(String another): 사전순 비교 (대소문자 구분)
변환 및 조작 (새 객체 반환)
String concat(String str): 문자열 합치기 (비효율 주의)
String replace(target, replacement): 문자열 치환
String substring(int beginIndex): 문자열 자르기
String toUpperCase() / toLowerCase(): 대소문자 변환
String trim(): 양쪽 공백 제거 (ASCII 공백만)
String strip() (Java 11+)
기타
int length(): 문자열 길이 반환
String intern(): 상수 풀에 등록된 문자열 참조를 반환 (강제 동기화, 자주 쓰이지 않음)
static String valueOf(type): 기본형을 문자열로 변환
static String format(String format, Object... args): 지정된 포맷으로 문자열 생성