JAVA의 가장 기본이 되는 내용

불변객체 (Immutable Object)

cwchoiit 2024. 4. 1. 09:25
728x90
반응형
SMALL

참고 자료:

 

김영한의 실전 자바 - 중급 1편 | 김영한 - 인프런

김영한 | 실무에 필요한 자바의 다양한 중급 기능을 예제 코드로 깊이있게 학습합니다., [사진]국내 개발 분야 누적 수강생 1위, 제대로 만든 김영한의 실전 자바[사진][임베딩 영상]단순히 자바

www.inflearn.com

불변객체가 필요한 이유

불변객체에 대해서 완전히 제대로 알아보자. 불변객체가 뭔지 알기 전에 불변 객체가 왜 필요한 지부터 알아야 한다. 자바에서 가장 크게 변수는 두 가지 타입이 있다.

  • 기본형 (Primitive type)
  • 참조형 (Reference type)

기본형은 값의 공유가 절대로 일어나지 않는다. 즉, 특정값을 어떤 기본형 변수에 넣으면 변수끼리 그 값을 공유할 수 있는 방법은 없다.

언제나 자바에서 대입은? 값을 복사해서 대입한다.

 

다음 코드를 보자. a라는 변수에 10을 담고 b라는 변수에 a를 대입했다. a와 b가 값을 공유하나? 아니다. 값을 복사해서 b에 넣어준 것뿐이다. 실제로 b를 변경해도 a에는 아무런 영향이 없다.

int a = 10;
int b = a;

 

정말로 그런지 확인해보자.

실행 결과:

a = 10
b = 10
a = 10
b = 20

 

b를 변경해도 a에는 아무런 영향이 없다.

 

근데, 참조형은 어떨까? 참조형은 참조값을 복사해서 대입한다. 코드로 보자.

 

Value

public class Value {
    private int value;

    public Value(int value) {
        this.value = value;
    }

    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }

    @Override
    public String toString() {
        return "Value{" +
                "value=" + value +
                '}';
    }
}

 

Main

public class Main {
    public static void main(String[] args) {
        Value v1 = new Value(3);
        Value v2 = v1;

        System.out.println(v1.getValue());
        System.out.println(v2.getValue());

        v2.setValue(10);
        System.out.println(v1.getValue());
        System.out.println(v2.getValue());
    }
}

 

Value라는 클래스의 인스턴스를 v1, v2로 만들었다. v2는 v1을 대입한다. 참조형은 참조값을 복사해서 대입한다. 즉, v1과 v2는 같은 메모리 주소를 공유한다. 이 상태에서 둘 중 하나의 값을 바꿔버리면 나머지 하나도 변경되는 상황이 생겨난다.

 

실행결과:

3
3
10
10

 

이걸 의도해서 만든 거라면 아무런 문제가 없지만, 그게 아니라면? 문제가 생기는 것이다. 특정 개발자는 3이라는 값을 가진 Value 인스턴스를 그저 복사해서 같은 3이라는 값을 가지는 새로운 인스턴스를 원했던 것인데 하나를 바꾸니 나머지 하나도 변경이 되는 것이다.

 

이걸 해결하는 근본적인 방법은 "새로운 인스턴스를 만드는 것이다". 다음이 그 예시이다.

public class Main {
    public static void main(String[] args) {
        Value v1 = new Value(3);
        Value v2 = new Value(5);

        System.out.println(v1.getValue());
        System.out.println(v2.getValue());

        v2.setValue(10);
        System.out.println(v1.getValue());
        System.out.println(v2.getValue());
    }
}

 

실행결과:

3
5
3
10

 

이제 v2값을 변경해도 v1에는 영향이 끼치지 않는다. 좋다. 근데 남아 있는 문제는 결국엔 두 객체 간 참조값 공유를 막을 방법은 없다는 것이다. 어떤 개발자가 다음과 같은 코드를 작성했을 때 이를 막아줄 방법이 없다. 왜냐면 문법적으로 잘못된 게 없으니까. 

Value v1 = new Value(3);
Value v2 = v1;

 

그러니까 결국 위와 같은 문제를 방지하려면 개발자가 알아서 이런 경우를 만들지 않도록 코드를 짜야하는데 가장 멀리해야 하는 게 스스로를 신뢰하는 것 아닌가? 저렇게 코드를 안 짜리라고 보장할 수 없다. 이걸 해결하기 위해 불변객체가 등장(?)한 것.

 

 

불변객체 (Immutable Object)

위 문제를 해결하기 위해 불변 객체라는 것이 사용된다. 불변 객체는 객체의 상태(객체 내부의 값, 필드, 멤버 변수)가 변하지 않는 객체를 불변 객체라고 한다. 사실 그렇다. 위 문제에서 참조값을 공유하는 것을 막을 수 있는 방법은 없다. 문법적으로도 잘못된 게 아니다. 그러면 근본적인 원인은 해결할 수가 없고, 대안을 찾아야 하는데 결국 문제가 발생하는 지점은 두 객체가 가지고 있는 값을 한 쪽에서 변경할 때 발생하지 않는가? 그럼 그 값을 변경하지 못하도록 막아버리는 방법이 있다. 그리고 이게 불변 객체이다.

 

그리고 실제로 참조값 공유는 유용하다. 만약 같은 값 3을 가지는 Value 인스턴스가 정말 필요하다면 굳이 새로운 인스턴스를 만들어서 괜히 메모리를 더 쓰는게 아니라 이미 가지고 있는 인스턴스를 공유하면 더 효율적이기 때문이다. 그러니 참조값 공유를 막는것을 생각하지 말고 객체가 가지는 상태를 변경하는 것을 막는것으로 바꾸는 것이다.

 

방법은 꽤나 간단하다. 필드를 final로 선언하고 setter를 빼버리면 된다.

public class Value {
    private final int value;

    public Value(int value) {
        this.value = value;
    }

    public int getValue() {
        return value;
    }

    @Override
    public String toString() {
        return "Value{" +
                "value=" + value +
                '}';
    }
}

 

이렇게 불변 객체로 만들면 한번 값이 적용된 후 값을 바꾸려고 하면 컴파일 에러가 발생한다.

 

위와 같은 에러가 발생하면 개발자는 "어? 하고 어떤 에러가 났는지 볼 것이고 보니까 아 이거 불변 객체구나! 값을 바꾸려면 새로 인스턴스를 만들어야겠다." 라는 생각을 할 수 있게 만들어 주는것.

 

 

불변객체 예시 코드

그럼 예시를 한번 만들어보자.

Member

public class Member {
    private String name;
    private Address address;

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

    public String getName() {
        return name;
    }

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

    public Address getAddress() {
        return address;
    }

    public void setAddress(Address address) {
        this.address = address;
    }

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

Address

public class Address {
    private String value;

    public Address(String value) {
        this.value = value;
    }

    public String getValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
    }

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

 

이 예시는 Member 클래스와 Address 클래스가 있을 때, Member 클래스에는 name, address라는 필드가 있다. 그래서 이 두개의 클래스를 가지고 다음과 같이 실행 코드를 만들었다고 해보자.

Main

public class Main {
    public static void main(String[] args) {
        Address address = new Address("서울");
        
        Member m1 = new Member("A", address);
        Member m2 = new Member("B", address);
    }
}

 

주소가 서울인 회원 두명이 있다. 이렇게 회원이 잘 만들어 졌는데 요구사항이 들어왔다. "회원 B의 주소를 서울에서 부산으로 변경해라."

그럼 개발자는 다음과 같은 행위를 한다.

m2.getAddress().setValue("부산");

 

좋다, 이제 그래서 다음과 같이 변경사항을 출력하는 코드까지 작성하고 실행 결과를 확인했다.

public class Main {
    public static void main(String[] args) {
        Address address = new Address("서울");

        Member m1 = new Member("A", address);
        Member m2 = new Member("B", address);

        System.out.println(m1);
        System.out.println(m2);

        System.out.println("주소 변경---------");

        m2.getAddress().setValue("부산");
        System.out.println(m1);
        System.out.println(m2);
    }
}

 

실행결과:

Member{name='A', address=Address{value='서울'}}
Member{name='B', address=Address{value='서울'}}
주소 변경---------
Member{name='A', address=Address{value='부산'}}
Member{name='B', address=Address{value='부산'}}

 

실행 결과를 보니 원하지 않는 결과가 발생했다. 회원 B의 주소만 변경했는데 회원 A의 주소까지 변경됐다. 

이제 이런 문제를 불변객체를 사용해서 방지해보자. Member는 불변객체가 아니다. Member 클래스의 필드(name, address)는 변경할 수 있어야한다. 

ImmutableAddress

public class ImmutableAddress {
    private final String value;

    public ImmutableAddress(String value) {
        this.value = value;
    }

    public String getValue() {
        return value;
    }
    
    @Override
    public String toString() {
        return "Address{" +
                "value='" + value + '\'' +
                '}';
    }
}

 

그래서 Address 클래스를 불변 객체로 변경했다. 필드에 final을 붙이고 setter를 떼버렸다. 이 클래스로 기존 코드를 대체해보자.

Main

그랬더니 다음과 같이 컴파일 에러가 발생한다. 개발자는 보고 어? 왜 에러가 발생했지?로 시작해서 아! 이거 불변객체라서 이렇게 사용못하는구나!"를 깨닫는다. 그래서 다음과 같은 코드로 대체한다.

그래서 결국 원하는 결과를 도출할 수 있었다.

 

실행결과:

Member{name='A', address=Address{value='서울'}}
Member{name='B', address=Address{value='서울'}}
주소 변경---------
Member{name='A', address=Address{value='서울'}}
Member{name='B', address=Address{value='부산'}}

 

 

불변객체의 값 변경

아무리 불변객체라고 해도 값을 변경하고 싶을때가 있다. 이럴땐 어떻게 하면 될까? 새로운 인스턴스를 반환하면 된다. 다음 코드를 보자.

ImmutableObj

public class ImmutableObj {
    private final int value;

    public ImmutableObj(int value) {
        this.value = value;
    }
    
    public ImmutableObj withValue(int value) {
        return new ImmutableObj(value);
    }

    public int getValue() {
        return value;
    }

    @Override
    public String toString() {
        return "ImmutableObj{" +
                "value=" + value +
                '}';
    }
}

 

불변 객체인 ImmutableObj 클래스가 가진 value라는 필드를 변경하고 싶은 경우 위 코드처럼 withValue()라는 메서드를 하나 만든다.

보통은 이렇게 불변객체에 새로운 값을 넣고 새로운 인스턴스를 반환하는 메서드를 만들 때 관례상 with___() 메서드 명을 따른다. 변경하고자 하는 값을 파라미터로 받아서 새로운 ImmutableObj 객체를 리턴하는 방식이다.

Main

public class Main {
    public static void main(String[] args) {
        ImmutableObj obj = new ImmutableObj(3);
        ImmutableObj obj2 = obj.withValue(10);

        System.out.println(obj);
        System.out.println(obj2);
    }
}

 

그래서 이 코드를 실행해보면 기존 객체는 값이 전혀 변경되지 않고, 새로운 객체를 만들어 새로운 값을 가지는 녀석으로 반환한다.

실행결과:

ImmutableObj{value=3}
ImmutableObj{value=10}
728x90
반응형
LIST

'JAVA의 가장 기본이 되는 내용' 카테고리의 다른 글

Method Chaining  (0) 2024.04.02
String 클래스  (0) 2024.04.01
java.lang 패키지와 그 첫번째 - Object  (0) 2024.03.31
OCP (Open-Closed Principle) 원칙  (0) 2024.03.30
다형성 (Part.2) 사용하기 ✨  (0) 2024.03.29