SSARTEL-10th / JPTS_bookstudy

"개발자가 반드시 알아야 할 자바 성능 튜닝 이야기" 완전 정복
7 stars 0 forks source link

String 은 왜 불변 객체여야 할까? #1

Open daminzzi opened 1 year ago

daminzzi commented 1 year ago

👍 문제!

Java 에서는 String을 불변 객체로 선언한다. 이로 인해 효율적인 String의 관리를 위해 StringBuilder 등을 사용하는데 처음부터 String이 불변객체가 아니라면 지금처럼 GC 관리와 같은 것을 생각할 필요가 없을텐데 왜 불변 객체로 만든걸까?

✈️ 선정 배경

자바에서 String을 다루는 것이 매우 복잡하다고 느끼고 있는데, 이 모든 게 자바가 String을 불변객체로 만들었기 때문이라는 생각이 들었다. 문제에서 이야기한것처럼 String을 보조하는 클래스까지 만들어가면서 불변객체로 선언한 이유는 무엇인지 생각해보면 좋을 것 같아서 해당 문제를 주제로 선정하였다.

📺 관련 챕터 및 레퍼런스

story3. 왜 자꾸 String을 쓰지 말라는 거야

🐳 비고

kgh2120 commented 1 year ago

들어가며

String은 Java에서 Object를 제외하고 제일 많이 쓰이는 클래스 중 하나입니다. 하지만 String을 다루기는 굉장히 귀찮은 일들이 많죠. 예를 들어서 StringBuilder를 통해서 문자열을 더해줘야 한다거나, String의 메서드를 이용하면, 반드시 리턴을 받아야 한다는 등 말입니다.

이런 일들이 발생하는 이유는, String이 불변 객체이기 때문입니다. String은 왜 불변 객체일까를 알아보기 앞서서 자바에서의 불변 객체가 무엇인지에 대해서 살펴보고, 불변 객체의 어떤 특징 때문에 String을 불변 객체로 만들었을지 알아보겠습니다.

불변 객체란?

Java에서의 불변 객체는 종류가 굉장히 많습니다. 대표적인 예시로는, 앞서 언급된 String부터, Integer, Long 등의 Wrapper 클래스들이 있습니다.

불변 객체는 말 그대로 내부의 상태가 변하지 않는 객체를 의미합니다. 이런 불변 객체는 어떻게 만들고, 어떤 장점이 있을까요?

불변 객체 생성법

불변 객체를 만들기 위해선 다음과 같은 원칙을 따르면 됩니다.

  1. setter 메서드를 제공하지 않고, read-only 메서드를 제공한다. 참조에 의해서 변경될 수 있는 경우엔 방어적 복사를 한다.
  2. 클래스를 fianl로 선언하라. (상속을 하지 못하게 한다)
  3. 모든 변수에 private, final을 선언한다. 참조 객체인 경우, 해당 객체도 불변 객체로 만든다.
  4. 객체 생성을 위한 생성자 혹은 정적 팩토리 메서드를 만든다.

위의 규칙을 잘 따르면 다음과 같은 불변 객체를 만들 수 있습니다.


public final class ImmutableStudent {

    private final String name;
    private final int age;
    private final Address address;

    private final List<ImmutableStudent> friends;

    private ImmutableStudent(String name, int age, Address address) {
        this.name = name;
        this.age = age;
        this.address = address;
        this.friends = new ArrayList<>();
    }

    public static ImmutableStudent of(final String name, final int age, final Address address) {
        return new ImmutableStudent(name, age, address);
    }

    public String getName() {
        return name;
    }
    public List<ImmutableStudent> getFriends() {
        return Collections.unmodifiableList(friends);
    }

    static final class Address{
        private final String sido;
        private final String gugun;
        private final String dong;

        public Address(String sido, String gugun, String dong) {
            this.sido = sido;
            this.gugun = gugun;
            this.dong = dong;
        }

        public static Address of(final String sido, final String gugun, final String dong) {
            return new Address(sido, gugun, dong);
        }
    }

}

불변 객체의 장점

불변 객체를 생성하는 방법을 알았으니, 이젠 불변 객체의 장점에 대해서 알아보겠습니다. 불변 객체의 장점으로 알려진 부분은 다음과 같습니다.

  1. Thread-Safe해서 멀티 쓰레드 환경에서 안전하다.
  2. 상태가 변하지 않아서 신뢰성이 상승한다.
  3. 내부 상태가 변하지 않기 때문에 Cache, Set, Map의 내부 요소로 활용 가능하다.
  4. GC의 성능을 향상시킬 수 있다.

1. Thread-Safe

멀티 쓰레드 환경에서 발생하는 동시성 문제는 write와 read가 동시에 발생할 때, 나타납니다. 하지만 불변 객체를 이용할 경우, write가 발생하지 않습니다.

2. 상태가 변하지 않아 신뢰성 상승

외부에서 객체의 값이 변하게 된다면, 객체의 상태를 예측하기와 변경 시점을 추적하기가 어렵습니다. 하지만 불변 객체의 경우 변하지 않은 상태를 가지기 때문에, 상태를 예측하기 쉽고, 오류가 발생할 경우 원인을 발견하기 쉽습니다.

아래와 같은 코드가 있다고 했을 때, 가변 객체의 경우, setter 메서드가 있기 때문에, 어떤 부분에서 KIM이라는 사람의 이름이 LEE로 바뀌었는지 알기 힘듭니다. 하지만 불변 객체의 경우 KIM이라는 사람의 이름이 LEE가 될 수 있는 경우는 객체가 생성되는 순간이기 때문에, 빠르게 원인 파악이 됩니다.

    public static void main(String[] args) {

        MutablePerson kim = new MutablePerson("KIM");
        // 1000 라인
        kim.setName("LEE");
        // 1000 라인
        System.out.println(kim.getName()); // 왜 이름이 LEE여?

        ImmutablePerson kimkim = new ImmutablePerson("LEE");

        // 10000 라인
        System.out.println(kimkim.getName()); // 왜 이름이 LEE여?

    }

    static class MutablePerson{
        String name;

        public MutablePerson(String name) {
            this.name = name;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }
    }

    static class ImmutablePerson{
        final String name;

        public ImmutablePerson(String name) {
            this.name = name;
        }

        public String getName() {
            return name;
        }
    }

3. Cache, Set, Map의 내부 요소로 활용

불변 객체는 Cache, Set, Map에서 사용될 경우, 이후 그 값이 변경될 일이 없어, Cache, Set, Map의 데이터를 갱신해주지 않아도 됩니다.

예를 들어 Integer가 불변 객체가 아닌 상황에서 Set<Integer>에 들어있다면, Set의 요소인 Integer가 외부에서 값이 변경될 때, Set의 특징이 유지되고 있는지 체크되어야 할 것입니다.

아래 예시를 보면 가변 객체인 경우, HashMap의 key와 HashSet의 요소의 value를 변경시키면서, HashMap의 key는 2를 꺼내올 수 없게 되었고, Set은 같은 요소가 있지 않는다는 특성이 깨지게 되었습니다.

    private static void testHashMap() {
        Map<MutableObject, Integer> map = new HashMap<>();
        MutableObject hello = new MutableObject("Hello");
        MutableObject hello2 = new MutableObject("Hello");
        MutableObject hi = new MutableObject("hi");
        map.put(hello, 1);
        map.put(hello2, 2);
        map.put(hi, 3);

        System.out.println(map); // {MutableObject{name='hi'}=3, MutableObject{name='Hello'}=2}
        hello.name = "hi";

        System.out.println(map); // {MutableObject{name='hi'}=3, MutableObject{name='hi'}=2}
        map.put(new MutableObject("hi"), 4);
        System.out.println(map); // {MutableObject{name='hi'}=4, MutableObject{name='hi'}=2}
        System.out.println(map.get(new MutableObject("hi"))); // 4
        System.out.println(map.get(hello));  // 4
    }

    private static void testHashSet() {
        Set<MutableObject> set = new HashSet<>();
        MutableObject hello = new MutableObject("Hello");
        MutableObject hello2 = new MutableObject("Hello");
        MutableObject hi = new MutableObject("hi");
        set.add(hello);
        set.add(hello2);
        set.add(hi);
        System.out.println(set); // [MutableObject{name='hi'}, MutableObject{name='Hello'}]
        hello.name = "hi";
        System.out.println(set); // [MutableObject{name='hi'}, MutableObject{name='hi'}]
    }

    static class MutableObject{
        String name;

        public MutableObject(String name) {
            this.name = name;
        }
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            MutableObject that = (MutableObject) o;
            return Objects.equals(name, that.name);
        }

        @Override
        public int hashCode() {
            return Objects.hash(name);
        }

        @Override
        public String toString() {
            return "MutableObject{" +
                    "name='" + name + '\'' +
                    '}';
        }
    }

4. GC의 성능 향상

객체의 값이 변경될 때, 가변 객체를 이용하는 것 보다 새로운 불변 객체를 이용하는 것이 GC의 성능을 더욱 향상할 수 있습니다. 이는 GC의 동작 방법 중 '최근에 생성된 객체는 일찍 사라진다.'에 따라서 오랫동안 유지된 가변 객체를 제거하는 것보다, 자주 불변 객체를 삭제하는 편이 더 좋습니다.(Heap의 yg 영역을 지우는 것이 og를 지우는 것보다 쉽다.)

그리고 오라클 문서에 따르면 객체를 만드는 것에 대한 비용이 과대평가 되어있고, 이는 불변 객체의 효율성으로 커버가 가능하다고 합니다.

Programmers are often reluctant to employ immutable objects, because they worry about the cost of creating a new object as opposed to updating an object in place. The impact of object creation is often overestimated, and can be offset by some of the efficiencies associated with immutable objects. These include decreased overhead due to garbage collection, and the elimination of code needed to protect mutable objects from corruption. 프로그래머는 객체를 제자리에서 업데이트하는 대신 새 객체를 생성하는 데 드는 비용을 걱정하기 때문에 불변 객체를 사용하는 것을 꺼리는 경우가 많습니다. 객체 생성의 영향은 종종 과대평가되며, 불변 객체와 관련된 일부 효율성으로 인해 상쇄될 수 있습니다. 여기에는 가비지 수집으로 인한 오버헤드 감소, 변경 가능한 개체를 손상으로부터 보호하는 데 필요한 코드 제거가 포함됩니다. - oracle https://docs.oracle.com/javase/tutorial/essential/concurrency/immutable.html

불변 객체 String

위의 내용을 통해 불변 객체의 장점에 대해서 알아보았다. 하지만 왜 String을 불변 객체로 이용해야 하는지에 대해서는 의문이 든다. String을 불변 객체로 이용해서 발생하는 불편을 많이 겪어보았기 때문이다.

장점

String을 불변 객체로 이용하는 이유는 크게 두 가지로, 성능보안이다.

성능

String은 자바에서 String Constant Pool에 의해서 관리됩니다. ""을 통해 생성된 String은 상수풀에 있는 객체를 할당받아 새로운 String을 생성하지 않습니다. 이를 통해 String을 이용할 때 성능을 향상하는데, 이때 String이 가변 객체라면 상수풀에 저장된 객체의 값이 변할 수 있기 때문에, 상수풀을 이용할 수 없을 것입니다.

보안

자바에서 String은 DB의 sql, 네트워크에서의 host, port, url등 민감한 정보를 담습니다. 이때 String이 가변 객체여서 이런 정보를 외부에서 변경한다면 심각한 피해가 발생할 것입니다.

아래 코드와 같이, second의 값이 변경되면 의도하지 않은 sql이 실행될 수 있습니다.

    private void excuteUpdate(String username) {
        String first = "UPDATE Customers SET Status = 'Active' ";
        String second = " WHERE UserName = '";
        String end = "'";
        String sql = first + second + username + end;

//        ....
        second = " WHERE UserId = '";

//        ....
    }

정리

'String은 왜 불변 객체일까?'라는 질문에 대해서 Java에서의 불변 객체의 정의와 특징을 알아보았고, String을 불변 객체로 삼으면서 얻을 수 있는 장점에 대해서 알아보았다. 정리하자면 아래와 같다.

불변 객체는 내부의 상태가 변하지 않는 객체이고, Thread-Safe 하며, Cache, Map, Set 등의 자료 구조에서 이용하기 좋고, 코드의 신뢰성을 보장하며, GC의 성능을 향상한다는 장점을 가진다.

String이 불변 객체인 이유는 상수풀을 이용할 수 있어 성능이 향상되고, 민감한 정보를 다루기 때문에, 보안이 좋아진다.

참고 자료

https://mangkyu.tistory.com/131 https://devoong2.tistory.com/entry/Java-%EB%B6%88%EB%B3%80-%EA%B0%9D%EC%B2%B4Immutable-Object-%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90 https://docs.oracle.com/javase/tutorial/essential/concurrency/immutable.html https://www.artima.com/articles/james-gosling-on-java-may-2001#part13 https://www.baeldung.com/java-string-immutable https://devlog-wjdrbs96.tistory.com/247 https://readystory.tistory.com/139