Understanding Immutable State in Java: When Why & How to Use It

Understanding Immutable State in Java: When Why & How to Use It

대부분의 프로그래밍 언어에는 시간이 흐르더라도 변하지 않는 개념 즉, 신뢰성(Reliability)과 효율성(Efficiency)을 들 수 있다. 이는 Immutability과 관련된 개념이다. Immutability 상태라 함은 Object에 속해있는 속성들이 Object 생성 이후부터는 변경되지 않는 상황(불변)을 말한다. 이와 반대로 Mutable 상태는 Object가 생성된 이후 속성들이 변경되는 상황을 말한다. 이 두 상태는 개발하다 보면 그에 상응하는 목적과 역할이 존재함을 알 수 있는데 이 글에서는 Immutable 상태에 초점을 맞추는 것으로 하며 다음과 같은 내용들을 얘기해 보고자 한다.

  • Java에서 immutable 상태가 왜 중요한가?
  • Java에서 immutable 상태를 어떻게 구현하는가?
  • Java에서 mutable상태 대신 언제 immutable 상태를 채택하는가?

Java에서 immutable상태가 왜 중요한가?

Immutability 상태는 Java 개발 관점에서 보면 매우 중요한 부분이며 소프트웨어 시스템의 효율성과 유지보수성, 신뢰성을 가름하는 기초적인 원칙으로 인식된다. 이는 많은 회사들이 Java 개발자를 뽑을 때 전문성 체크를 목적으로 immutable state를 다룰 줄 아는지를 체크하는 이유이기도 하다. Java언어 개발 영역에서 immutability가 가져다 주는 여러 장점을 들자면 다음과 같다.

Thread Safety

Immutability는 복잡한 synchronization 처리 필요성을 제거함으로써 thread safety를 보장해 준다. 상태가 immutable이 되면 여러 thread가 데이터에 접근하더라도 데이터의 integrity를 해치지 않고 안전하게 접근하거나 공유할 수 있도록 한다.

Predictability

Immutability는 코드의 예측 가능성(predictability)성을 높여주며 이해하기 쉽도록 한다. 객체가 생성될 때 이 객체의 모든 속성들이 immutable로 선언되면 이 속성들의 값들은 변경되지 않는 상태로 유지되며 따라서 이들의 동작도 예측하기 쉬워진다.

Concurrency

Java에서 Immutability는 공유상태(shared state)관리를 위해 필요한 복잡성을 줄여줌으로 concurrent 프로그래밍을 수월하게 한다. Immutable은 초기 생성된 이후로 그 상태가 변경되지 않기 때문에 개발자들이 concurrent 환경에서 mutable상태를 관리하다 범하게 되는 일반적인 개발 실수를 하지 않도록 해준다.

Performance Tuning

Immutable 상태에서는 새로운 객체를 생성함으로 인해 메모리 사용율을 높게 만들기는 하지만 다른 방식으로 접근해보자면 성능 최적화에 기여할 수도 있다. Java에서 immutable 객체는 안전하게 공유하거나 캐싱(Caching)할 수 있어 방어적인 복사와 Synchronization 처리에 대한 부담을 줄여준다.

Debugging and Maintenance

Immutable 상태에서는 예측이 수월하고 객체의 속성들이 변하지 않기 때문에 디버깅을 위한 상태를 변경하더라도 보다 안전하다. 개발자들은 독립적으로 문제에 접근하고 확인할 수 있으며 이슈를 immutable 객체로 한정시킬 수 있다. 이렇게 하면 디버깅을 빠르게 할 수 있으며 전체 코드베이스에 대한 유지보수를 더욱 용이하게 해 준다.

Java에서 immutable 상태를 어떻게 구현하는가?

Java 개발에서 Immutability를 위해 객체의 Lifecycle 동안 상태가 고정될 수 있도록 하는 주요 원칙(Key Principle)과 Best Practice를 따르도록 권고한다. Java 코드에서 immutability를 적용할 수 있는 단계별 가이드는 아래와 같다.

1. Declare Classes as Final

클래스를 상속하지 못하도록 final로 선언한다. 이렇게 하면 클래스의 동작과 상태는 변하지 않는 상태로 남게된다.

public final class Point {
// Class definition
}

2. Make Fields Private

Field를 private으로 선언하면 클래스 밖에서의 접근을 차단시킬 수 있다. 이렇게 하면 데이터의 캡슐화(encapsulation)을 향상시킬 수 있다.

public final class Point {
    private final int x;
    private final int y;
    // Class definition
}

3. Avoid providing Setter Methods

상태에 따라 해당 값이 바뀌는 것을 바라지 않을 수 있다. 따라서 setter메소드를 추가하지 않는다면 객체가 생성된 이후 값을 변경하지 못할 것이다.

public final class Point {
    private final int x;
    private final int y;
   
    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
    Use getter methods mentioned in step 6
}

4. Make Mutable Fields Final (if needed)

Point라는 클래스가 있다고 할 경우 이 클래스가 mutable인 Address 클래스의 객체를 reference로 가지는 상황이 발생할 수 있다. 이 경우 immutability를 보장하기 위해 아래와 같이 할 수 있다.

public final class Point {
    private final int x;
    private final int y;
    private final Address address; //assuming address class is mutable

    public Point(int x, int y, Address address) {
        this.x = x;
        this.y = y;
        //Defensive copy prevents modification of original address object
        this.address = new Address{address.getStreet() , address.getCity() );
    }
    /// Class Definition

여기서 address 필드는 final로 선언되었지만 원본 Address객체의 상태를 변경하지 않도록 하기 위해 생성자에서 deep copy를 수행한다.

5. Initialize Fields using a Constructor with Deep Copy (if required)

이 단계는 클래스가 다른 객체의 reference를 가질 경우에 해당한다. 앞의 4번 샘플에서 보면 생성자에서 Address객체의 복사본을 생성하는 것을 보여주었다.

6. Provide Getter Methods that return out Objects

값 ‘y’를 직접적으로 반환하는 대신 getter 메소드를 만들고 여기에서 복사본을 반환하도록 한다 이렇게 하면 부주의하게 내부 상태를 변경하는 것을 방지할 수 있다. (primitive 타입은 call by value가 값이 복사된 후 리턴된다)

public final class Point {
    private final int x;
    private final int y;
 
    publicPoint(int x, int y) {
        this.x = x;
        this.y.= y;
    }
    public int getX() {
        return  x;
    }
    public int getY() {
        Returning a copy to avoid medication of original value
        return y;
    }
}

Java에서 mutable상태 대신 언제 immutable 상태를 채택하는가?

지금까지 immutability 상태가 왜 중요한지 그리고, 어떻게 구현하는지 알았보았다. 하지만 Java에서 언제 mutable 상태 대신 immutable 상태를 선택해야 할까? 프로젝트에서 아래와 같이 Immutable 상태가 도움이 되는 이상적인 시나리오가 있다.

Thread Safety

기본적으로 Immutable 객체는 thread-safe하다. 이들의 상태는 변하지 않기 때문에 synchronization이슈에 고민이 필요 없으며 다수의 thread가 동일한 immutable객체에 접근할 수 있다. 이렇게 하면 concurrent 프로그래밍을 단순하게 해 주며 race condition의 발생 위험을 줄여준다.

Referential Transparency

Immutable객체는 referential transparency를 향상시켜 준다. 따라서, 만약 두 객체가 동일한 상태를 가지고 있다면 이 두 객체는 동일하다고 간주할 수 있다. 이렇게 하면 이들 객체에 대한 연산이 어떻게 수행되는지 유추하기가 쉬워진다. 이는 동작 과정에서 객체의 상태가 변경되지 않음을 보장해 주기 때문이다.

Predictability and Debugging

Immutable객체의 항시성(constant state)은 이들 동작에 대한 예측을 더욱 용이하도록 만든다. 객체가 한번 생성되면 immutable 객체는 내부의 값을 변경되거나 수정되지 않아 코드의 동작을 쉽게 유추할 수 있게 한다. 이는 디버깅을 쉽게 해 주고 reference를 따라가며 객체의 상태를 쉽게 추적할 수 있도록 한다.

Caching

Immutable 객체는 예상치 못한 이유로 상태가 변경되는 것을 방지하기 때문에 Caching의 요구사항으로 이상적이라 할 수 있다. 어떤 기능이 Immutable 객체의 특정 값을 자주 사용한다면 Outdate되는 걱정 없이 그 값를 캐싱시킬 수 있다.

Functional Programming

Immutability는 functional programming에서의 핵심 Principle 중 하나이다. 이는 immutability와 순수 function의 개념을 강조한다. 객체를 immutable로 만듦으로써 더욱 functional한 코드를 작성할 수 있게하며 테스트와 원인분석을 더욱 쉽도록 한다.

마치며…

Java에서 상태의 immutability는 두 가지 역할 즉, 데이터 무결성 보호와 thread safety를 보장 측면에서 매우 중요한 개념이다. 객체의 상태가 생성 이후 변경되지 않는다는 것을 보장한다면 개발자들은 객체가 Lifecycle 동안 변하지 않는 상태로 유지됨을 확신할 수 있다. 이는 버그의 발생 위험을 줄여주고 thread safety를 개선하며 concurrent 프로그래밍을 단순하게 해 준다. 더욱이, immutability를 적용하면 functional 프로그래밍을 가능하게 하고 더욱 모듈화 되고 확장 가능한 소프트웨어 아키텍처로 이어갈 수 있도록 한다. 면밀한 설계와 immutability 원칙에 대한 통찰력을 바탕으로 하면 숙련된 Java 개발자들은 어플리케이션 개발 과정에서 충분한 잠재력을 발휘할 수 있을 것이다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다