Java

Java의 불변 객체

정의

객체 생성 이후, 내부 데이터를 변경할 수 없는(immutable) 객체

예시

String, Integer, Boolean 등

특징

  • read-only 메소드만을 제공
  • 객체의 내부 상태를 제공하는 메소드를 제공하지 않거나, 방어적 복사(defensive-copy)를 통해 제공
  • 얻는 이익에 비해, 성능 상의 단점은 미미

장점

  • 안전
    • Immutable이기 때문에 안전하다. 몇 개의 예시를 살펴보자.
        1. 여러 참조 변수가 같은 String 객체를 참조하고 있다고 하더라도 안전하다. String 객체를 누가 조작할 수가 없기 때문이다.
        1. String 객체를 이리저리 전달할 때 원본 String 객체가 저장된 주소 자체를 넘겨도 안전하다. 전달받은 곳에서 원본 값을 직접 읽을 순 있으나 조작할 수는 없기 때문이다.
  • 성능 및 효율
    • 여러 참조 변수가 같은 String 객체를 참조하고 있다고 하더라도 안전하다
      • Java는 힙 영역 안에 String 객체들을 모아 두고(String object pool) 같은 String 값이 여러 참조 변수에 의해 참조될 수 있도록 할 수 있었다.
      • 예를 들어 "java"라는 문자열을 필요로 하는 곳이 100개라면, 100개의 "java" 문자열을 만드는 것이 아니라 하나의 "java" 문자열을 pool 안에 두고 100개가 pool 안에 있는 하나의 "java" 문자열을 참조하도록 하여 성능과 메모리 효율면에서 이득을 얻었다.
      • notion image
    • String 객체를 이리저리 전달할 때 원본 String 객체가 저장된 주소 자체를 넘겨도 안전하다.
      • 만약 String이 mutable 했다면 A에서 B로 String을 전달할 때 String을 전달할 때 B가 String을 수정할 수 없도록 하기 위해 String을 복사한 뒤 복사된 String을 B에게 전달했을 것이다.
      • 하지만 immutable이기 때문에 원본 Stirng의 주소를 넘길 수 있고, 전달할 때마다 String이 복사되는 것이 아니기 때문에 성능에서 이득을 얻을 수 있다.
    • Immutable이기 때문에 개발자가 신경써야 할 부분이 줄어든다는 것 역시 이점으로 볼 수 있다.
      • 다른 곳에서 String을 바꾸지 못하도록 신경 쓰지 않아도 되고
      • 멀티 쓰레드 환경이라고 하더라도 String에 추가적으로 신경 써줄 필요가 없다.

목적

  • Thread-Safe하여 병렬 프로그래밍에 유용하며, 동기화를 고려하지 않아도 된다.
    • final class를 이용하기 때문
    • 멀티 쓰레드 환경에서 동기화 문제가 발생하는 이유는 공유 자원에 동시에 쓰기(Write) 때문이다.
    • 하지만 만약 공유 자원이 불변이라면 더 이상 동기화를 고려하지 않아도 될 것이다.
    • 왜냐하면 항상 동일한 값을 반환할 것이기 때문이다.
    • 이는 안정성을 보장할 뿐만 아니라 동기화를 하지 않음으로써 성능상의 이점도 가져다준다.
  • 실패 원자적인(Failure Atomic) 메소드를 만들 수 있다.
    • 불변 객체라면 어떠한 예외가 발생하여도 메소드 호출 전의 상태를 유지할 수 있을 것이다.
    • 그리고 예외가 발생하여도 오류가 발생하지 않은 것 처럼 다음 로직을 처리할 수 있다.
  • Side Effect를 피해 오류가능성을 최소화할 수 있다.
    • 불변 객체는 기본적으로 값의 수정이 불가능하기 때문에 변경 가능성이 적으며, 객체의 생성과 사용이 상당히 제한된다.
    • 그렇기 때문에 메소드들은 자연스럽게 순수 함수들로 구성될 것이고, 다른 메소드가 호출되어도 객체의 상태가 유지되기 때문에 안전하게 객체를 다시 사용할 수 있다.
    • 이러한 불변 객체는 오류를 줄여 유지보수성이 높은 코드를 작성하도록 도와줄 것이다.
  • 다른 사람이 작성한 함수를 예측가능하며 안전하게 사용할 수 있다.
    • 불변성이 보장된 함수라면 다른 사람이 개발한 함수를 위험없이 이용할 수 있다.
    • 마찬가지로 다른 사람도 내가 작성한 메소드를 호출하여도, 값이 변하지 않음을 보장받을 수 있다.
    • 그렇기에 우리는 변경에 대한 불안없이 다른 사람의 코드를 이용할 수 있다.
    • 또한 불필요한 시간을 절약할 수도 있는데, 이에 대한 예제는 아래에서 자세히 살펴보도록 하자.
  • 가비지 컬렉션의 성능을 높일 수 있다.

면접 질문

  1. 불변 객체는 무엇인가요?
    1. 객체 생성 이후, 내부 데이터를 변경할 수 없는(immutable) 객체
  1. 불변 객체의 특징 몇 가지 설명해주세요.
  1. 불변 객체로 얻을 수 있는 이점이 무엇인가요?
    1. Thread-Safe하여 병렬 프로그래밍에 유용하며, 동기화를 고려하지 않아도 된다.
    2. Side Effect를 피해 오류가능성을 최소화할 수 있다
    3. 다른 사람이 작성한 함수를 예측가능하며 안전하게 사용할 수 있다.
  1. 불변 객체로 인한 단점에는 무엇이 있을까요?
    1. 모든 객체의 불변성을 보장하게 된다면, 상태 변화가 필요한 경우 새로운 객체를 생성해야 한다는 단점이 있고, 새로운 객체를 많이 생성하는 경우 성능 문제가 발생할 수 있다.
    2. 하지만 Oracle에 의하면, 객체 생성 비용에 대한 영향은 종종 과대평가되며, 불변 객체를 활용할 때의 이점들이 이런 단점을 상쇄시킨다고 합니다.
  1. 객체 내 모든 필드를 final로 선언하였습니다. 그럼에도 불구하고 불변성이 보장되지 않았습니다. 원인을 분석해주시고 해결방법을 제안해주세요.
    1. 인스턴스 변수가 특정 객체를 참조하는 참조 변수라면, 참조 대상이 바뀔 수 없다는 뜻일 뿐 참조하고 있는 객체 자체의 불변성을 보장할 수는 없습니다.
    2. 이런 경우, 참조하고 있는 객체 또한 immutable 하게 사용할 수 있게끔 처리를 해주어야 합니다.
  1. 위의 방법을 제외하고, 객체를 Immutable하게 쓰는 방법에 어떤 것들이 있을까요?
    1. setter 메소드를 제공하지 않는 방법
      1. 상태값을 변경하지 않기 위해 setter 메소드를 제공하지 않는다.
    2. final class로 선언
      1. 클래스 자체를 final로 선언하면 해당 클래스를 다른 클래스에서 상속받는 게 불가능하기 때문에, 부모 클래스에 선언되어 있는 메소드 오버라이딩이 불가합니다.
      2. 하지만 상속이 불가능하다는 것 뿐이지 해당 클래스의 객체들이 immutability를 보장하지는 않습니다.
      3. 따라서, 선언한 final class에 a번 setter 메소드를 제공하지 않는 방법을 함께 사용해야 setter를 사용한 상태값 수정을 막을 수 있습니다.
    3. 모든 mutable 필드를 final로 선언
      1. Java에서 변수를 final로 선언하면 해당 변수를 초기화할 때 할당된 값을 변경할 수 없게 됩니다.
      2. 인스턴스 변수가 primitive type이라면 final로 선언하여 불변성을 유지할 수 있습니다.
      3. 하지만 인스턴스 변수가 특정 객체를 참조하는 참조 변수라면, 참조 대상이 바뀔 수 없다는 뜻일 뿐 참조하고 있는 객체 자체의 불변성을 보장할 수는 없습니다.
      4. 이런 경우, 참조하고 있는 객체 또한 immutable 하게 사용할 수 있게끔 처리를 해주어야 합니다.
    4. 번외
      1. 모든 필드의 접근제어자를 private으로 선언
        1. 클래스의 모든 필드를 private으로 선언하면 해당 클래스만 해당 필드에 대한 접근권한을 가지게 됩니다.
        2. 클래스에 setter 메소드가 없다면 private으로 선언해 외부 클래스로부터의 접근을 차단할 수 있습니다.
      2. 생성자를 통해 초기화되는 필드들은 깊은 복사를 통한 참조 대상 재할당
        1. 생성자를 통해 초기화되는 인스턴스 변수들이 reference 변수라면 깊은 복사를 통해 참조하는 객체 내부의 값이 변하는 것을 방지할 수 있습니다.
      3. getter 메소드를 객체의 깊은 복사본을 반환하도록 한다.
        1. getter 메소드는 실제 객체에 대한 reference를 반환하는 대신 깊은 복사를 통해 생성한 객체에 대한 reference를 반환하여, 반환받은 객체를 사용할 때 실수로라도 기존 객체를 건드릴 일이 없게끔 만들어줄 수 있습니다.
  1. String은 불변객체인가?
    1. 변수에 할당되면 참조를 업데이트하거나 내부 상태를 어떤 방법으로도 변경할 수 없기 때문
      1. 만약 String이 불변객체라면 가변객체로 활용하는 방법는 무엇인가?
        1. StringBuffer
        2. StringBuilder
  1. String을 불변 객체로 만든 이유에 대해 생각해보고, 본인 의견 두개 이상 말씀해주세요.
    1. String이 불변 객체이기 때문에 String Pool도 존재할 수 있다.
      1. 값이 같은 String은 같은 String Pool 내에서 String 객체를 바라본다.
      2. 만약 String 타입이 mutable 하다면,
      3. 한 값을 다른 값으로 바꿨을 때,
      4. 값이 다른데 같은 참조를 가지게 되어 불가능한 상황이 된다.
    2. 불변 객체는 값이 바뀔 일이 없기 때문에 멀티스레드 환경에서 Thread-safe 하다는 장점이 있습니다.
      1. 따라서 일반적으로 불변 객체는 동시에 실행되는 여러 스레드에서 공유할 수 있습니다. 스레드가 값을 변경하면 동일한 문자열을 수정하는 대신 문자열 풀에 새 문자열이 생성되기 때문에 스레드 안전할 수 있습니다.
    3. 문자열은 Java 애플리케이션에서 사용자 이름, 암호, 연결 URL, 네트워크 연결 등과 같은 중요한 정보를 저장하는 데 널리 사용된다.
      1. 클래스를 로드하는 동안 JVM 클래스 로더에서도 광범위하게 사용됩니다.
      2. 따라서 String 클래스 보안은 일반적으로 전체 응용 프로그램의 보안에 매우 중요합니다.
      3. 만약에 String이 불변 객체가 아니라면 메소드를 호출했던 클라이언트는 String에 대한 참조를 계속 가지고 있기 때문에 문자열을 변경할 수 있다는 가능성이 남아있습니다.
      4. 따라서, 이 문자열이 안전하다고 보장할 수 없습니다.
  1. 멀티스레드환경에서 안전하게 String을 사용하는 방법은?
    1. StringBuffer에 syncronized 키워드 사용
  1. String이 불변객체를 유지하는 원리는?
    1. java heap에서 StringPool을 사용하고
    2. String 클래스는 intern() 메소드로 이를 사용한다.
  1. 생성비용이 비싼, 하지만 케이스마다 생성할수 밖에없는  Pattern, Matcher Class 의 경우 [정규식 관련 클래스], 어떤 방식으로 불변객체를 활용해야할까? 물론 정규식을 사용해야하므로 가변객체로 전환은 불가능하다.
    1. 필드캐싱(static final), 더블체크 라킹, 지연초기화로 가능하다.

출처