Java 프로그래밍에서의 불변 문자열

JavaBeginner
지금 연습하기

소개

이 랩에서는 Java 프로그래밍에서 불변 (immutable) 문자열을 사용하는 이점에 대해 배웁니다. Java 에서 문자열이 기본적으로 불변인 이유와 이 특성이 애플리케이션의 전반적인 성능과 보안을 어떻게 향상시킬 수 있는지 이해하게 될 것입니다. 이 랩이 끝나면 Java 프로그램에서 불변 문자열을 생성하고 효과적으로 사용하는 방법을 알게 될 것입니다.

Java 프로젝트 파일 생성

이 단계에서는 ~/project 디렉토리에 ImmutableStringDemo.java라는 새 Java 파일을 생성합니다. 이 파일에는 이 랩의 모든 Java 코드가 포함됩니다.

먼저 ~/project 디렉토리에 있는지 확인합니다. pwd 명령을 사용하여 현재 디렉토리를 확인할 수 있습니다.

pwd

다음과 유사한 출력이 표시되어야 합니다.

/home/labex/project

이제 touch 명령을 사용하여 ImmutableStringDemo.java 파일을 생성합니다.

touch ImmutableStringDemo.java

파일을 생성한 후 WebIDE 편집기에서 열어 Java 코드를 작성할 수 있습니다.

문자열 불변성과 String Pool 이해하기

Java 에서 String 객체는 불변 (immutable) 입니다. 즉, 생성된 후에는 값을 변경할 수 없습니다. 이러한 설계 선택은 보안, 스레드 안전성 및 문자열 풀을 통한 성능 최적화를 포함한 여러 이점을 제공합니다.

문자열 풀 (String pool) 은 힙 (heap) 내의 특별한 메모리 영역으로, 문자열 리터럴 (literal) 을 저장합니다. 문자열 리터럴을 생성할 때 Java 는 먼저 풀에 동일한 문자열이 이미 존재하는지 확인합니다. 존재한다면 기존 문자열의 참조가 반환되어 메모리가 절약됩니다. 존재하지 않는다면 풀에 새 문자열이 생성되고 해당 참조가 반환됩니다.

Java 클래스의 기본 구조를 추가하고 문자열 생성을 시연해 보겠습니다. WebIDE 에서 ImmutableStringDemo.java를 열고 다음 코드를 추가합니다.

// ~/project/ImmutableStringDemo.java
public class ImmutableStringDemo {

    public static void main(String[] args) {
        // 문자열 리터럴 생성
        String name = "LabexUser";
        System.out.println("Initial name: " + name);

        // 문자열 풀을 사용할 수 있는 문자열 생성
        String str1 = "Hello";
        String str2 = "Hello"; // 이것은 문자열 풀에서 str1 과 동일한 객체를 참조할 가능성이 높습니다.

        System.out.println("str1: " + str1);
        System.out.println("str2: " + str2);
        System.out.println("str1 == str2: " + (str1 == str2)); // 동일한 객체를 참조하는지 확인

        // 'new' 키워드를 사용하여 문자열 생성
        // 이것은 리터럴이 풀에 존재하더라도 항상 힙에 새 객체를 생성합니다.
        String str3 = new String("Hello");
        System.out.println("str3: " + str3);
        System.out.println("str1 == str3: " + (str1 == str3)); // 동일한 객체를 참조하는지 확인
    }
}

코드를 추가한 후 파일을 저장합니다.

이제 Java 프로그램을 컴파일하고 실행하여 출력을 확인합니다. 터미널에서 다음 명령을 실행합니다.

javac ImmutableStringDemo.java
java ImmutableStringDemo

다음과 유사한 출력이 표시되어야 합니다.

Initial name: LabexUser
str1: Hello
str2: Hello
str1 == str2: true
str3: Hello
str1 == str3: false

이 출력은 str1str2가 문자열 풀에서 동일한 객체를 참조하는 반면, str3(new String()으로 생성됨) 은 내용이 동일하더라도 힙에 별도의 객체임을 보여줍니다.

보안 및 스레드 안전성을 위한 불변성 적용

불변성 (Immutability) 은 Java 에서 보안 및 스레드 안전성을 위해 매우 중요합니다. 문자열이 불변이면, 생성 후 프로그램의 어떤 부분에서도 해당 값을 변경할 수 없습니다. 이는 악성 코드가 비밀번호나 URL 과 같은 민감한 데이터를 저장하는 문자열을 변경하는 것을 방지합니다.

또한, 불변 객체는 본질적으로 스레드 안전합니다. 여러 스레드가 외부 동기화 없이 불변 문자열에 동시에 접근할 수 있습니다. 이는 한 스레드가 문자열을 수정하는 동안 다른 스레드가 읽는 위험이 없기 때문입니다.

문자열에 final 키워드를 추가하고 간단한 스레드 안전 예제를 만들어 이를 시연해 보겠습니다.

ImmutableStringDemo.java 파일을 다음과 같이 수정합니다. ThreadSafeTask 클래스에서 접근할 수 있도록 클래스 수준에서 final 문자열을 선언할 것입니다.

// ~/project/ImmutableStringDemo.java
import java.util.HashMap;

public class ImmutableStringDemo {

    // 보안 및 스레드 안전성을 위한 final 문자열 선언
    private static final String SECURE_PASSWORD = "MySecurePassword123";

    // 스레드 안전성을 시연하기 위한 내부 클래스
    static class ThreadSafeTask implements Runnable {
        private final String password; // 비밀번호는 불변 문자열로 전달됩니다.

        public ThreadSafeTask(String password) {
            this.password = password;
        }

        @Override
        public void run() {
            System.out.println("Thread " + Thread.currentThread().getName() + " accessing password: " + password);
            // 작업 시뮬레이션
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }

    public static void main(String[] args) {
        // ... (이전의 name, str1, str2, str3 코드는 여기에 유지됩니다.) ...
        String name = "LabexUser";
        System.out.println("Initial name: " + name);

        String str1 = "Hello";
        String str2 = "Hello";
        System.out.println("str1: " + str1);
        System.out.println("str2: " + str2);
        System.out.println("str1 == str2: " + (str1 == str2));

        String str3 = new String("Hello");
        System.out.println("str3: " + str3);
        System.out.println("str1 == str3: " + (str1 == str3));

        System.out.println("\n--- 보안 및 스레드 안전성 시연 ---");

        // final 문자열 사용
        System.out.println("Secure Password: " + SECURE_PASSWORD);

        // 불변 비밀번호로 스레드 안전성 시연
        for (int i = 0; i < 3; i++) {
            Thread thread = new Thread(new ThreadSafeTask(SECURE_PASSWORD), "Thread-" + (i + 1));
            thread.start();
        }

        // 스레드가 완료될 때까지 대기 (선택 사항, 더 깔끔한 출력을 위해)
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

참고: final 키워드는 SECURE_PASSWORD 변수에 한 번만 할당될 수 있음을 보장합니다. String 자체가 불변이므로, 이는 SECURE_PASSWORD의 내용을 변경할 수 없게 만듭니다. ThreadSafeTask 클래스는 여러 스레드에서 이 불변 문자열에 안전하게 접근합니다.

파일을 저장합니다. 그런 다음 프로그램을 다시 컴파일하고 실행합니다.

javac ImmutableStringDemo.java
java ImmutableStringDemo

스레드 접근 메시지가 비결정적인 순서로 나타나는 것을 포함하여 다음과 유사한 출력이 표시되어야 합니다.

Initial name: LabexUser
str1: Hello
str2: Hello
str1 == str2: true
str3: Hello
str1 == str3: false

--- Demonstrating Security and Thread Safety ---
Secure Password: MySecurePassword123
Thread Thread-1 accessing password: MySecurePassword123
Thread Thread-2 accessing password: MySecurePassword123
Thread Thread-3 accessing password: MySecurePassword123

컬렉션에서 해시코드 캐싱을 위한 불변성 활용

문자열의 불변성은 HashMapHashSet과 같은 해시 기반 컬렉션에서 키로 사용하는 데 이상적입니다. 문자열의 값은 절대 변경되지 않으므로, hashCode()를 한 번만 계산하여 캐싱할 수 있습니다. 이후 hashCode() 호출 시 캐싱된 값을 반환하여, 특히 문자열이 키로 자주 사용될 때 상당한 성능 향상을 가져옵니다.

만약 문자열이 가변적이라면, HashMap에 삽입된 후 해시코드가 변경될 수 있어 원래 해시코드를 사용하여 객체를 검색하는 것이 불가능해집니다.

HashMap에서 문자열 사용을 시연하기 위해 ImmutableStringDemo.java에 코드를 추가해 보겠습니다.

스레드 안전성 시연 후, ImmutableStringDemo.java 파일의 main 메서드에 다음 코드를 추가합니다.

// ~/project/ImmutableStringDemo.java
import java.util.HashMap; // 이 import 문이 파일 상단에 있는지 확인하세요.

public class ImmutableStringDemo {

    private static final String SECURE_PASSWORD = "MySecurePassword123";

    static class ThreadSafeTask implements Runnable {
        private final String password;

        public ThreadSafeTask(String password) {
            this.password = password;
        }

        @Override
        public void run() {
            System.out.println("Thread " + Thread.currentThread().getName() + " accessing password: " + password);
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }

    public static void main(String[] args) {
        // ... (이전의 name, str1, str2, str3 및 스레드 안전성 코드는 여기에 유지됩니다.) ...
        String name = "LabexUser";
        System.out.println("Initial name: " + name);

        String str1 = "Hello";
        String str2 = "Hello";
        System.out.println("str1: " + str1);
        System.out.println("str2: " + str2);
        System.out.println("str1 == str2: " + (str1 == str2));

        String str3 = new String("Hello");
        System.out.println("str3: " + str3);
        System.out.println("str1 == str3: " + (str1 == str3));

        System.out.println("\n--- Demonstrating Security and Thread Safety ---");
        System.out.println("Secure Password: " + SECURE_PASSWORD);

        for (int i = 0; i < 3; i++) {
            Thread thread = new Thread(new ThreadSafeTask(SECURE_PASSWORD), "Thread-" + (i + 1));
            thread.start();
        }
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        System.out.println("\n--- Demonstrating Hashcode Caching ---");

        HashMap<String, Integer> studentScores = new HashMap<>();

        String studentName1 = "Alice";
        String studentName2 = "Bob";
        String studentName3 = "Alice"; // 이것은 문자열 풀의 동일한 "Alice"를 참조합니다.

        studentScores.put(studentName1, 95);
        studentScores.put(studentName2, 88);
        studentScores.put(studentName3, 92); // 이것은 "Alice"의 값을 업데이트합니다.

        System.out.println("Student Scores: " + studentScores);
        System.out.println("Alice's score: " + studentScores.get("Alice"));
        System.out.println("Bob's score: " + studentScores.get("Bob"));

        // 문자열을 "변경"하려고 해도 새 문자열이 생성됨을 시연합니다.
        String originalString = "Java";
        System.out.println("Original String: " + originalString + ", HashCode: " + originalString.hashCode());

        String modifiedString = originalString.concat(" Programming"); // 새 문자열 생성
        System.out.println("Modified String: " + modifiedString + ", HashCode: " + modifiedString.hashCode());
        System.out.println("Original String (after concat): " + originalString + ", HashCode: " + originalString.hashCode());
    }
}

파일을 저장합니다. 그런 다음 프로그램을 다시 컴파일하고 실행합니다.

javac ImmutableStringDemo.java
java ImmutableStringDemo

HashMap 및 해시코드 시연을 포함하여 다음과 유사한 출력이 표시되어야 합니다.

Initial name: LabexUser
str1: Hello
str2: Hello
str1 == str2: true
str3: Hello
str1 == str3: false

--- Demonstrating Security and Thread Safety ---
Secure Password: MySecurePassword123
Thread Thread-1 accessing password: MySecurePassword123
Thread Thread-2 accessing password: MySecurePassword123
Thread Thread-3 accessing password: MySecurePassword123

--- Demonstrating Hashcode Caching ---
Student Scores: {Bob=88, Alice=92}
Alice's score: 92
Bob's score: 88
Original String: Java, HashCode: 2301506
Modified String: Java Programming, HashCode: -1479700901
Original String (after concat): Java, HashCode: 2301506

concat이 호출된 후에도 originalString의 해시 코드가 동일하게 유지되는 것을 알 수 있습니다. 이는 concat새로운 String 객체를 반환하고 원본은 그대로 두기 때문입니다.

요약

이 실습에서는 Java 프로그래밍에서 불변 문자열 (immutable Strings) 의 근본적인 개념을 학습했습니다. 문자열이 기본적으로 불변인 이유와 이러한 설계 선택이 문자열 풀 (String pool) 을 통한 메모리 최적화에 어떻게 기여하는지 살펴보았습니다. 또한, 민감한 데이터에 대한 보안 강화, 동시 작업에 대한 내재된 스레드 안전성, 해시코드 캐싱으로 인한 해시 기반 컬렉션에서의 성능 향상 등 불변성의 중요한 이점을 이해했습니다. 실습 과제를 완료함으로써 Java 애플리케이션에서 불변 문자열을 효과적으로 생성하고 활용하는 실질적인 경험을 쌓았습니다.