[Java] String 정리

2025. 12. 16. 22:26·공부일기../Java

1. String

Java에서 String은 가장 많이 사용되는 클래스 중 하나다.

문자열을 다루는 것은 프로그래밍의 기본이지만, String은 다른 일반 객체들과는 조금 다른 특성을 가지고 있다.

 

먼저 String은 불변 객체(Immutable Object)다. 한번 생성된 String 객체의 내용은 절대 변경할 수 없다.

예를 들어 "hello"라는 문자열을 만들었다면, 이 문자열의 내용 자체를 "world"로 바꿀 수는 없다.

대신 새로운 String 객체를 만들어야 한다.

String str = "hello";
str = str + " world";  // "hello" 객체는 그대로, 새로운 "hello world" 객체 생성

위 코드에서 + 연산을 하면 기존 "hello" 문자열이 변경되는 것이 아니라, "hello world"라는 완전히 새로운 String 객체가 만들어지고 str 변수가 이 새 객체를 가리키게 된다. 기존 "hello" 객체는 메모리에 그대로 남아있다가 아무도 참조하지 않으면 가비지 컬렉터에 의해 정리된다.

 

String이 불변인 이유는 여러 가지가 있다.

첫째, String Pool을 통한 메모리 최적화가 가능하다.

둘째, 멀티스레드 환경에서 안전하다. 여러 스레드가 동시에 같은 String 객체를 참조해도 내용이 변하지 않으니 동기화 걱정이 없다.

셋째, HashMap이나 HashSet의 키로 사용할 때 안전하다. 만약 String이 가변이라면 키로 사용 중인 문자열이 변경되어 해시값이 달라지면 큰 문제가 발생한다.

 

 

 

2. String Pool과 메모리 관리

Java는 String을 효율적으로 관리하기 위해 String Pool이라는 메모리 영역을 사용한다.

String Pool은 Heap 메모리의 영역으로 같은 내용의 문자열 리터럴을 재사용하기 위해 존재한다.

String s1 = "hello";
String s2 = "hello";
String s3 = new String("hello");

System.out.println(s1 == s2);  // true
System.out.println(s1 == s3);  // false
System.out.println(s1.equals(s3));  // true

위 코드를 보면 s1과 s2는 같은 참조를 가리킨다. 리터럴로 "hello"를 생성하면 JVM은 먼저 String Pool에 "hello"가 있는지 확인한다. 이미 있다면 새로 만들지 않고 기존 객체의 참조를 반환한다. 따라서 s1과 s2는 완전히 같은 객체를 가리키므로 == 비교가 true다.

 

반면 s3는 new 연산자로 생성했기 때문에 String Pool이 아닌 일반 Heap 영역에 새로운 객체가 만들어진다. 따라서 내용은 같지만 s1과 s3는 서로 다른 객체이므로 == 비교는 false가 된다. 하지만 equals() 메서드는 참조가 아닌 실제 내용을 비교하므로 true를 반환한다.

실무에서는 가능한 한 리터럴 방식으로 String을 생성하는 것이 좋다. 메모리도 절약되고 성능도 더 좋다.

new String()은 정말 필요한 경우가 아니면 사용하지 않는다.

 

 

 

3. intern() 메서드로 String Pool 활용

new 연산자로 생성한 String을 나중에 String Pool에 등록하고 싶다면 intern() 메서드를 사용할 수 있다.

String s1 = new String("hello");
String s2 = s1.intern();
String s3 = "hello";

System.out.println(s1 == s2);  // false
System.out.println(s2 == s3);  // true

s1.intern()을 호출하면 JVM은 String Pool에 "hello"가 있는지 확인한다. 있다면 그 참조를 반환하고, 없다면 s1의 내용을 String Pool에 등록하고 그 참조를 반환한다. s3는 리터럴이므로 당연히 String Pool의 "hello"를 가리킨다. 따라서 s2와 s3는 같은 객체다.

intern()은 대량의 중복 문자열을 다룰 때 유용하다. 예를 들어 데이터베이스에서 수천 개의 레코드를 읽어오는데 특정 컬럼의 값이 자주 중복된다면, intern()을 사용해 메모리를 크게 절약할 수 있다. 하지만 무분별하게 사용하면 String Pool이 너무 커져서 오히려 성능이 저하될 수 있으니 주의해야 한다.

 

 

 

4. Java 9 이후의 Compact Strings

Java 9부터 String의 내부 구조가 바뀌었다. Java 8까지는 String이 내부적으로 char[] 배열을 사용했다.

char는 2바이트이므로 모든 문자가 2바이트를 차지했다. 하지만 영어나 숫자 같은 ASCII 문자는 1바이트로 충분하므로 메모리 낭비가 심했다.

Java 9부터는 byte[] 배열과 encoding flag를 함께 사용하는 Compact Strings 방식을 도입했다. 문자열의 모든 문자가 Latin-1(ISO-8859-1) 범위 내에 있다면 1바이트씩만 사용하고, 그렇지 않으면 UTF-16으로 2바이트씩 사용한다.

String s1 = "hello";     // Latin-1로 표현 가능 → 5바이트
String s2 = "안녕하세요";  // 한글 포함 → UTF-16, 10바이트

이 변화는 개발자가 직접 생각해야 할 필요는 없다. JVM이 알아서 처리한다. 하지만 영문이 주로 사용되는 환경에서는 메모리 사용량이 거의 절반으로 줄어들 수 있다는 점은 알아두면 좋다.

 

 

 

5. String은 참조 타입이다

String은 클래스이므로 참조 타입(Reference Type)이다. int, char, boolean 같은 기본형(Primitive Type)과는 근본적으로 다르다.

int num = 10;
String str = "hello";

// num.length();  // 컴파일 에러! 기본형은 메서드가 없다
str.length();     // 5, 가능! 객체이므로 메서드를 가진다

기본형은 값 자체를 저장하지만, 참조 타입은 객체의 주소를 저장한다.

따라서 String 변수는 실제 문자열 데이터가 아니라 그 데이터가 있는 메모리 위치를 가리킨다.

참조 타입이기 때문에 null을 할당할 수 있다.

String str = null;  // 가능
// int num = null;  // 컴파일 에러!

또한 Object 클래스를 상속받으므로 toString(), equals(), hashCode() 같은 메서드를 사용할 수 있고, 다형성도 적용된다.

Object obj = "문자열";  // String은 Object의 자식이므로 가능

 

 

 

6. String의 주요 메서드들

String 클래스는 문자열을 다루기 위한 다양한 메서드를 제공한다. 자주 사용되는 메서드들을 카테고리별로 정리하면 다음과 같다.

 

길이와 검색 관련 메서드

length()는 문자열의 길이를 반환한다. 주의할 점은 메서드라는 것이다. 배열의 length는 필드지만 String의 length()는 메서드다.

String str = "Hello World";
System.out.println(str.length());  // 11 (공백 포함)

charAt(int index)는 특정 위치의 문자를 반환한다. 인덱스는 0부터 시작한다.

char ch = str.charAt(0);  // 'H'
// char ch = str.charAt(11);  // StringIndexOutOfBoundsException!

indexOf()와 lastIndexOf()는 특정 문자나 문자열이 처음 또는 마지막으로 등장하는 위치를 반환한다. 없으면 -1을 반환한다.

int index1 = str.indexOf("o");      // 4 (첫 번째 'o')
int index2 = str.lastIndexOf("o");  // 7 (마지막 'o')
int index3 = str.indexOf("xyz");    // -1 (없음)

contains()는 특정 문자열이 포함되어 있는지 boolean으로 반환한다.

boolean result = str.contains("World");  // true

startsWith()와 endsWith()는 특정 문자열로 시작하거나 끝나는지 확인한다.

str.startsWith("He");   // true
str.endsWith("ld");     // true

 

변환 관련 메서드

toLowerCase()와 toUpperCase()는 모든 문자를 소문자나 대문자로 변환한다. 원본은 변하지 않고 새로운 String을 반환한다.

String lower = str.toLowerCase();  // "hello world"
String upper = str.toUpperCase();  // "HELLO WORLD"
System.out.println(str);           // "Hello World" (원본 유지)

trim()은 문자열 앞뒤의 공백을 제거한다. Java 11부터는 strip()을 사용할 수 있는데, trim()보다 더 광범위한 유니코드 공백 문자를 처리한다.

String str2 = "  hello  ";
System.out.println(str2.trim());   // "hello"
System.out.println(str2.strip());  // "hello" (Java 11+)

replace()는 특정 문자나 문자열을 다른 것으로 바꾼다. replaceAll()은 정규표현식을 사용할 수 있다.

String result = str.replace("World", "Java");  // "Hello Java"
String result2 = "a1b2c3".replaceAll("\\d", "");  // "abc"

substring()은 문자열의 일부를 잘라낸다.

String sub1 = str.substring(0, 5);   // "Hello" (0~4번 인덱스)
String sub2 = str.substring(6);      // "World" (6번부터 끝까지)

 

분리와 결합 관련 메서드

split()은 문자열을 특정 구분자로 나눠서 배열로 반환한다.

String[] words = str.split(" ");  // ["Hello", "World"]
String csv = "사과,바나나,포도";
String[] fruits = csv.split(",");  // ["사과", "바나나", "포도"]

join()은 정적 메서드로, 배열이나 여러 문자열을 하나로 합친다.

String result = String.join(", ", "사과", "바나나", "포도");
// "사과, 바나나, 포도"

String[] arr = {"a", "b", "c"};
String result2 = String.join("-", arr);  // "a-b-c"

 

비교 관련 메서드

equals()는 내용을 비교한다. ==는 참조를 비교하므로 문자열 비교에는 항상 equals()를 사용해야 한다.

String s1 = new String("hello");
String s2 = new String("hello");
System.out.println(s1 == s2);      // false (다른 객체)
System.out.println(s1.equals(s2)); // true (같은 내용)

equalsIgnoreCase()는 대소문자를 구분하지 않고 비교한다.

"Hello".equalsIgnoreCase("hello");  // true

compareTo()는 사전순으로 비교한다. 같으면 0, 앞이면 음수, 뒤면 양수를 반환한다.

"apple".compareTo("banana");  // 음수 (apple이 앞)
"zoo".compareTo("abc");       // 양수 (zoo가 뒤)

 

 

 

7. 메서드 체이닝 원리

String의 메서드들은 대부분 새로운 String 객체를 반환한다.

이 특성 덕분에 메서드를 연속으로 호출하는 메서드 체이닝(Method Chaining)이 가능하다.

String result = "  Hello World  "
    .trim()              // "Hello World"
    .toLowerCase()       // "hello world"
    .replace("world", "java");  // "hello java"

각 메서드가 실행될 때마다 새로운 String 객체가 만들어지고, 그 객체에 대해 다음 메서드가 호출된다.

 

하지만 주의할 점이 있다. 메서드 체이닝을 할 때마다 중간에 임시 String 객체들이 계속 생성된다. 위 예제에서는 총 3개의 중간 객체가 만들어진다. 체이닝이 길어질수록 메모리와 성능에 영향을 줄 수 있다.

 

StringBuilder도 메서드 체이닝을 지원하는데, 차이점은 StringBuilder는 새 객체를 만들지 않고 자기 자신(this)을 반환한다는 것이다.

StringBuilder sb = new StringBuilder("hello")
    .append(" ")
    .append("world")
    .reverse();
String result = sb.toString();  // "dlrow olleh"

StringBuilder의 체이닝은 중간 객체가 생성되지 않으므로 훨씬 효율적이다.

 

 

 

8. String 연산의 성능 문제

String은 불변이므로 + 연산을 할 때마다 새로운 객체가 생성된다.

소량의 연산이라면 문제없지만, 반복문 안에서 계속 + 연산을 하면 성능 문제가 발생한다.

// 나쁜 예
String result = "";
for (int i = 0; i < 1000; i++) {
    result += i;  // 1000번의 새 String 객체 생성!
}

위 코드는 반복문이 돌 때마다 새로운 String 객체를 만든다. i가 0일 때 "0", i가 1일 때 "01", i가 2일 때 "012" 이런 식으로 계속 새 객체를 만든다. 총 1000개의 String 객체가 생성되고, 이전 객체들은 모두 가비지가 된다.

더 큰 문제는 시간 복잡도다. 매번 기존 문자열 전체를 복사하므로 O(n²) 시간이 걸린다. 1000번이면 약 50만 번의 문자 복사가 발생한다.

// 좋은 예
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
    sb.append(i);  // 내부 버퍼에 추가만 함
}
String result = sb.toString();

StringBuilder는 내부적으로 가변 배열(버퍼)을 가지고 있어서 append()할 때 기존 데이터를 복사하지 않는다. 단순히 버퍼 끝에 추가만 한다. 버퍼가 가득 차면 자동으로 크기를 늘린다. O(n) 시간에 처리된다.

컴파일러가 똑똑해서 단순한 + 연산은 자동으로 StringBuilder로 변환해준다.

String result = "hello" + " " + "world";
// 컴파일러가 자동으로 아래처럼 변환
String result = new StringBuilder().append("hello").append(" ").append("world").toString();

하지만 이 최적화는 반복문 안에서는 적용되지 않는다. 반복문 안에서는 개발자가 직접 StringBuilder를 사용해야 한다.

 

 

 

9. StringBuilder vs StringBuffer

문자열을 조합할 때 사용하는 가변 클래스는 두 가지가 있다. StringBuilder와 StringBuffer다.

StringBuilder는 thread-unsafe하다. 멀티스레드 환경에서 여러 스레드가 동시에 같은 StringBuilder 객체를 수정하면 데이터가 깨질 수 있다. 하지만 그만큼 빠르다.

StringBuffer는 thread-safe하다. 모든 메서드가 synchronized로 동기화되어 있어서 멀티스레드 환경에서 안전하다. 하지만 동기화 오버헤드 때문에 StringBuilder보다 느리다.

// 단일 스레드 환경 (대부분의 경우)
StringBuilder sb = new StringBuilder();
sb.append("hello");

// 멀티스레드 환경
StringBuffer sb = new StringBuffer();
sb.append("hello");

실무에서는 StringBuilder를 훨씬 많이 사용한다. 대부분의 경우 문자열 조합은 메서드 내부의 지역 변수로 수행되고, 지역 변수는 스레드마다 독립적이므로 동기화가 필요 없다. StringBuffer가 필요한 경우는 정말 여러 스레드가 같은 객체를 공유하며 수정할 때뿐이다.

성능 차이는 상황에 따라 다르지만, StringBuilder가 대략 10~30% 정도 빠르다고 알려져 있다.

 

 

 

10. String.format()으로 문자열 포매팅

String.format()은 C 언어의 printf와 유사한 방식으로 문자열을 포매팅하는 정적 메서드다. 여러 값을 조합해 문자열을 만들 때 + 연산보다 가독성이 훨씬 좋다.

String name = "홍길동";
int age = 25;

// + 연산 (가독성 떨어짐)
String msg1 = "이름: " + name + ", 나이: " + age;

// String.format (가독성 좋음)
String msg2 = String.format("이름: %s, 나이: %d", name, age);

 

주요 포맷 지정자

  • %s: String (문자열)
  • %d: decimal (정수)
  • %f: float (실수)
  • %c: character (문자)
  • %b: boolean
  • %n: 줄바꿈 (OS에 따라 \n 또는 \r\n)
String.format("문자열: %s", "hello");           // "문자열: hello"
String.format("정수: %d", 42);                  // "정수: 42"
String.format("실수: %f", 3.14);                // "실수: 3.140000"
String.format("실수: %.2f", 3.14159);           // "실수: 3.14" (소수점 2자리)
String.format("문자: %c", 'A');                 // "문자: A"
String.format("불린: %b", true);                // "불린: true"

자릿수와 정렬을 지정할 수도 있다.

String.format("%5d", 42);        // "   42" (5자리, 오른쪽 정렬)
String.format("%-5d", 42);       // "42   " (5자리, 왼쪽 정렬)
String.format("%05d", 42);       // "00042" (5자리, 0으로 채움)
String.format("%10.2f", 3.14);   // "      3.14" (총 10자리, 소수점 2자리)

여러 값을 한 번에 포매팅할 수 있다.

String result = String.format(
    "%s님의 점수는 %d점이고, 평균은 %.1f점입니다.",
    "철수", 90, 85.5
);
// "철수님의 점수는 90점이고, 평균은 85.5점입니다."

실무에서는 로그 메시지, SQL 쿼리 생성, 사용자 메시지 등을 만들 때 자주 사용한다. 하지만 주의할 점이 있다. String.format()은 내부적으로 정규표현식을 사용하므로 단순 + 연산보다 느리다. 성능이 중요한 반복문 안에서는 StringBuilder를 사용하는 것이 좋다.

 

 

 

11. 주의사항

NPE(NullPointerException) 조심하기

String은 참조 타입이므로 null일 수 있다. null인 String에 메서드를 호출하면 NPE가 발생한다.

String str = null;
// int len = str.length();  // NullPointerException!

// 안전한 방법 1: null 체크
if (str != null && str.length() > 0) {
    // ...
}

// 안전한 방법 2: 상수를 앞에
if ("constant".equals(str)) {  // str이 null이어도 안전
    // ...
}

상수나 확실히 null이 아닌 값을 equals()의 주체로 두면 NPE를 피할 수 있다.

 

빈 문자열 체크

빈 문자열("")과 null은 다르다. 빈 문자열은 길이가 0인 유효한 String 객체다.

String str1 = "";
String str2 = null;

str1.isEmpty();  // true
// str2.isEmpty();  // NullPointerException!

// Java 11+
str1.isBlank();  // true (공백만 있어도 true)
"   ".isBlank(); // true

isEmpty()는 길이가 0인지만 확인하고, isBlank()는 공백 문자만 있어도 true를 반환한다.

 

== vs equals()

문자열 비교는 항상 equals()를 사용해야 한다. ==는 참조를 비교하므로 예상치 못한 결과가 나올 수 있다.

String s1 = "hello";
String s2 = new String("hello");

if (s1 == s2) {  // false! 다른 객체
    // 실행 안 됨
}

if (s1.equals(s2)) {  // true! 같은 내용
    // 실행됨
}

리터럴끼리는 String Pool 때문에 ==가 true일 수도 있지만, 이에 의존하면 안 된다. 항상 equals()를 사용하자!!!!!

 

 

String은 간단해 보이지만 내부 동작을 이해하면 더 효율적이고 안전한 코드를 작성할 수 있다~ 끗

'공부일기.. > Java' 카테고리의 다른 글

[디자인패턴] 자바 전략 패턴(Strategy Pattern)  (0) 2025.11.25
[java] 예외 처리와 트랜잭션 롤백 (PSA (Portable Service Abstraction))  (0) 2025.10.29
[java] Enum 추상 메서드 활용과 이해  (0) 2025.10.03
[코테] JAVA 백준 알고리즘 시작하기 - 입출력 가이드 !!성능최적화!!  (0) 2025.09.29
[java] 자바 파일 처리 기본기 가이드 - 주니어 개발자 필수 FILE/FILES  (0) 2025.09.22
'공부일기../Java' 카테고리의 다른 글
  • [디자인패턴] 자바 전략 패턴(Strategy Pattern)
  • [java] 예외 처리와 트랜잭션 롤백 (PSA (Portable Service Abstraction))
  • [java] Enum 추상 메서드 활용과 이해
  • [코테] JAVA 백준 알고리즘 시작하기 - 입출력 가이드 !!성능최적화!!
s0-0mzzang
s0-0mzzang
공부한것을 기록합니다...
  • s0-0mzzang
    승민이의..개발일기..🐰
    s0-0mzzang
  • 전체
    오늘
    어제
    • 전체~ (108)
      • 마음가짐..! (10)
      • 공부일기.. (76)
        • weekly-log (6)
        • Spring (19)
        • Java (18)
        • DataBase (10)
        • git (2)
        • JPA (6)
        • kafka (1)
        • Backend Architecture (3)
        • Troubleshooting (삽질..ㅋ) (2)
        • Cloud (1)
        • Docker (2)
        • 알고리즘 (1)
        • 리액트 (2)
        • Infra (3)
      • 하루일기.. (22)
        • 그림일기 (8)
        • 생각일기 (14)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • 깃허브
  • 공지사항

  • 인기 글

  • 태그

    ADC 환경
    자바
    MySQL
    스프링부트
    spring boot
    다짐
    항해99
    리팩토링
    항해플러스
    BufferedReader
    SpringBoot
    인프라 기초
    StringTokenizer
    Paging
    spring
    React
    TDD
    JPA
    swagger
    ERD
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
s0-0mzzang
[Java] String 정리
상단으로

티스토리툴바