본문 바로가기
Web

[Java] 불변 객체(Immutable Object)

by DuncanKim 2022. 8. 7.
728x90

[Java] 불변 객체(Immutable Object)

 

어떻게 사랑이 변하니?

 

 

개발자인 우리는 불변이라고 하면 final이 먼저 생각날 것이다. 자바의 final은 한 번만 할당이 가능하다는 것을 알고 있을 것이다. 재할당을 하려고 하면 컴파일 오류가 당연히 생길 것이다. 이것은 변하면 안 되는 변수, 메서드, 클래스에 예약어로 붙어서 우리가 코드를 구현할 때 실수하지 않게, 로직에만 집중할 수 있도록 도와준다.

 

 

1. 불변 객체(Immutable Object)란?

 

그렇다면 이번 포스팅에서 설명할 불변 객체는 무엇일까?


이해를 쉽게 하기 위해서는 반대 개념이 있는지를 먼저 살펴보는 것도 좋다. 그렇다고 하면, '가변 객체'가 있는 지를 짧게 생각해보자. 우리는 클래스를 만들기도 하고, 만들어져있는 클래스를 가져와서 객체로 만들기도 한다. ArrayList의 객체를 생성해서 첫번째 자리에 값을 넣는다던지, 우리가 임의로 정의하는 클래스를 생성해서 객체를 만든다던지, 그러한 객체 안의 필드값을 바꾼다던지 하는 개발행위를 한다. 그렇다. 이것이 메서드, 필드, 생성자가 변하는 가변 객체이다. 우리는 흔히 '가변 객체'를 사용하고 있는 것이다. 

 

그럼 이것들이 바뀌지 않는 것이 불변 객체일 것이다. 클래스의 인스턴스가 생성된 이후에 내부의 생성자, 메서드, 필드를 변경할 수 없는 객체를 의미한다. String 클래스가 대표적인 불변 객체를 만드는 클래스이다. 커스텀 클래스의 경우에도 내부의 어떤 것이 변경되지 않게 만들면, 그것도 불변 객체를 만들 수 있는 클래스가 될 것이다.

 


++ 

String의 경우,

 String a = "a";
 a = "b";

이렇게 되니까 객체가 변하는 것 아니냐는 생각을 할 수 있다. 그렇지만, 값을 바꾸는 경우, String은 새 객체를 만들어 그 값을 참조하는 형식이라, 객체가 변하는 것이 아니고, '새 객체가 생성되어 객체 참조 변수가 값을 갖는다'라고 생각해야 한다.


 

 

2. 불변 객체의 사용이유(장점, 단점)

 

불변 객체를 사용하는 이유는 무엇일까? 장점을 알아보면 될 것이다.


 

1) 멀티 스레드 환경에서 성능, 안전상의 이점을 가진다.

 

멀티 스레드 환경에서 동기화 문제는 '공유 자원' 때문에 일어난다. 불변 객체를 사용한다면, 항상 동일한 값만 반환하므로 동기화를 신경쓰지 않아도 된다. 결과적으로 데이터가 엉키는 문제도 없고 읽고 쓰는 시간도 절약될 수 있다.

 

 

2) 예외 상황에서 추가 에러 방지

 

가변 객체의 경우 변경된 상태로 인해서 예외가 발생하면 그 상태로 인해 추가적인 에러가 발생할 수 있다. 그렇지만 불변 객체의 경우 어떤 예외가 발생해도 메서드 호출 전의 상태를 유지할 수 있어서 예외가 발생해도 변경된 상태로 인한 추가 에러가 생기지 않는다.

 

 

3) Cache, Map, Set 등의 요소로 활용하기 적합하다.

 

캐시나 맵, Set 등으로 사용되는 객체가 변경되었다면 이를 갱신하는 작업이 필요하다. 하지만, 객체가 불변이라면 데이터가 저장된 이후에 부가 작업을 고려하지 않아도 되고, 캐시나 다른 자료 구조를 사용하는데 용이하다.

 

 

4) 변화로 인한 사이드 이펙트를 피할 수 있다.

 

사이드 이펙트는 변수의 값이 변경되거나 필드 값이 설정되는 등의 변화가 부수적으로 일어나는 효과를 말한다. setter가 구현되어 있고 여러 메서드에서 객체의 값이 변경된다면, 객체의 상태를 예측하기 어려워진다. 이를 방지하기 위해 사이드 이펙트가 없는 순수한 함수를 만드는 것이 중요하다고 할 수 있다. 이에 불변 객체는 변경 불가능한 요소들을 클래스로 구성해서 만들어 진 것이기 때문에, 객체를 안전하게 재사용할 수 있고, 사이드 이펙트로 인한 오류를 줄어줘서 유지보수의 효율에도 도움을 준다.

 

 

5) 협업에 있어서 안전한 객체 사용이 가능하다.

 

불변객체는 값이 변하지 않음을 보장하기 때문에, 다른 사람의 코드를 변경할 때 '이것 때문에는' 오류가 일어나지 않을 것이라는 예측을 하고 사용할 수 있다.

 

 

6) GC의 성능을 높일 수 있다.

 

불변 객체를 활용하면 가비지 컬렉터가 스캔해야 하는 객체의 수가 줄어든다. 불변 객체를 참조하고 있는 어떤 객체들이 있을 것이다. 만약 그 객체들까지 GC가 검사를 통해서 죽은 데이터들을 골라낸다고 생각해보자. 그렇지만, 불변 객체를 먼저 검사하고, 그것이 실행이 되고 있는 상황이라면? 그것을 참조하고 있는 객체들은 지우면 안되는 것이 되는 것이다.

 

따라서 불변 객체의 생존여부를 먼저 검사하기 때문에, 가비지 컬렉터가 검사를 해야하는 힙 영역의 객체 수가 줄어들게 되고, 따라서 성능 향상에 도움을 줄 수 있다는 결론을 낼 수 있는 것이다.

 

 


++ 단점

객체가 가지는 값마다 새로운 객체가 필요하다. 그래서 메모리 누수와 새로운 객체를 계속 생성해야하기 때문에 성능저하가 일어날 수 있다.


 

 

3. 불변 객체의 구현

 

쉽게 생각해보면, private, final을 붙이면 외부에서 변경 불가능하기 때문에 불변 객체가 된다고 할 수 있다.

당연히 getter, setter도 못쓰기 때문에, 내부 인자들이 변할 수가 없다.


final을 남발하고, static 메서드를 추가하고, 만약, 필드에 가변 객체가 포함된다면, 방어적 복사를 이용해서 전달해야 한다.

 

1) 기본 구현

class final ImmutablePerson {
    private final int age;
    private final int name;
    
    public ImmutablePerson(int age, int name) {
    	this.age = age;
        this.name = name;
    }
}

 

 

2) 방어적 복사

 

public class Member {
    private final String name;

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

 

new ArrayList 로 새로운 객체를 만들어 방어적 복사를 수행한다.

public class VIPMembers {
    private final List<Member> members;

    public VIPMembers(List<Member> members) {
        this.members = new ArrayList<>(members);
    }
}

 

이렇게 방어적 복사를 하면, memberList의 Member 객체와 vipMembers의 내부 Member 객체가 서로 다른 주소값을 가지게 된다.

public class Application {
    public static void main(String[] args) {
        List<Member> memberList = new ArrayList<>();
        memberList.add(new Member("박선생"));
        memberList.add(new Member("이선생"));

        VIPMembers vipMembers = new VIPMembers(memberList);
        memberList.add(new Member("김선생"));
    }
}

VIPMember 내부 필드에 Member 객체가 생성되지만, memberList 안의 객체와 주소값을 공유하지 않게 되는 것이다.

 

 

 


방어적 복사를 안하는 케이스는 어떤 코드를 가지나?

public class VIPMembers {
    private final List<Member> members;

    public VIPMembers(List<Member> members) {
        this.members = members;
    }
}

VIPMembers 클래스 안의 생성자에 차이가 있다. 여기에서는 members를 그대로 this.members로 받는 모습을 볼 수 있는데, 이렇게 되면 같은 주소값을 공유하게 된다. 따라서 인자로 들어오는 members가 수정되면 내부 필드의 값도 같이 변경이 된다.


 

정리해서 말하면, 방어적 복사란, 객체 내부에서 직접 파라미터를 받아 필드 값에 집어넣지 않고, 새롭게 객체를 새로 생성해서 필드 값에 집어넣는 방식을 쓰는 방식을 말한다. 여기에서 불변 객체는 vipMembers라는 객체 참조 변수 안에 담겨 있는 것이다.

 

728x90

댓글