Stay Hungry Stay Foolish

자바/자바 중급

[자바 중급] 02. 불변 객체

dev스카이 2024. 9. 28. 17:10

기본형과 참조형의 공유

자바의 데이터 타입은 크게 기본형(Primitive Type)과 참조형(Reference Type)으로 나눌 수 있다.

  • 기본형 : 하나의 값을 여러 변수에서 절대로 공유하지 않는다.
  • 참조형 : 하나의 객체를 참조값을 통해 여러 변수에서 공유할 수 있다.

 

공유 참조와 사이드 이펙트

사이드 이펙트(Side Effect)는 프로그래밍에서 어떤 계산이 주된 작업 외에 추가적인 부수 효과를 일으키는 것을 말한다.

사이드 이펙트는 프로그래밍에서는 부정적인 의미로 사용된다. 프로그램의 특정 부분에서 발생한 변경이 의도치 않게 다른 부분에 영향을 미치기 때문에 디버깅이 어려워지고 코드의 안정성이 저하될 수 있다.

 

사이드 이펙트가 발생한 경우의 예제

package lang.immutable.address;

public class RefMain1_1 {
    public static void main(String[] args) {
        //참조형 변수는 하나의 인스턴스를 공유할 수 있다.
        Address a = new Address("서울");
        Address b = a;
        System.out.println("a = " + a);
        System.out.println("b = " + b);

        b.setValue("부산"); //b의 값을 부산으로 변경해야함
        System.out.println("부산 -> b");
        System.out.println("a = " + a); //사이드 이펙트 발생
        System.out.println("b = " + b);
    }
}

 

객체의 주소를 보관하는 Address 클래스를 만들고 b의 값만 부산으로 변경하려고 할 때, 실행 결과는 원하는 결과와 다르게 나온다. 

 

실행 결과

a = Address{value='서울'}
b = Address{value='서울'}
부산 -> b
a = Address{value='부산'}
b = Address{value='부산'}

 

위와 같은 결과가 나온 이유는 참조형 변수들은 같은 참조값을 통해 같은 인스턴스를 참조할 수 있기 때문이다. 'b = a' 라고 했기 때문에 a에 있는 참조값을 복사해서 b에 전달하게 된다. 따라서 a와 b의 참조값이 같아서 b의 값만 변경한다고 해도 a값도 같이 변경된다.

 

이 문제를 해결하는 방법은 간단하다. 다음을 보자.

 

사이드 이펙트 해결 방안 예제

package lang.immutable.address;

public class RefMain1_2 {
    public static void main(String[] args) {
        Address a = new Address("서울");
        Address b = new Address("서울"); //변경한 코드
       
        System.out.println("a = " + a);
        System.out.println("b = " + b);

        b.setValue("부산"); //b의 값을 부산으로 변경해야함
        System.out.println("부산 -> b");
        System.out.println("a = " + a); //사이드 이펙트 발생
        System.out.println("b = " + b);
    }
}

 

참조값을 복사해서 전달하는 코드를 지우고, a와 b가 처음부터 서로 다른 인스턴스를 참조하는 코드로 수정하면 b의 값만 부산으로 변경되는 결과를 확인할 수 있다.

 

실행 결과

a = Address{value='서울'}
b = Address{value='서울'}
부산 -> b
a = Address{value='서울'}
b = Address{value='부산'}

 

위와 같은 결과로 확인해 볼 수 있는 것은 같은 객체(인스턴스)를 여러 변수가 함께 공유하면 사이드 이펙트가 발생하지만, 객체를 공유하지 않으면 문제가 발생하지 않는다. 따라서 서로 다른 객체를 참조하면 된다.

 

 

🔎 근본적인 원인

그러나 객체를 여러 변수에서 공유해서 발생한 문제를 막을 수 있는 방법은 없다. Main에서 개발자가 참조값 대입 코드를 언제든 적을 수 있기 때문이다. 사이드 이펙트의 더 근본적인 원인을 고려해보면 객체를 공유하는 것 자체가 문제가 아니고, 문제의 직접적인 원인은 공유된 객체의 값을 변경한 것에 있다.

 

서로 다른 인스턴스를 사용하는 것보다 인스턴스가 하나인 것이 메모리 절약과 성능상 효율적이다. 사이드 이펙트가 발생한 곳은 값을 변경한 부분이다. 따라서 문제의 직접적인 원인은 공유될 수 있는 Address 객체의 값을 어디선가 변경했기 때문이라고 말할 수 있다. 만약 Address 객체의 값을 변경하지 못하게 설계했다면 이런 사이드 이펙트 자체가 발생하지 않을 것이다. 다음을 보며 위와 같은 문제를 해결해보자.

 

 

불변 객체

객체의 상태(객체 내부의 값, 필드, 멤버 변수)가 변하지 않는 객체를 불변 객체(Immutable Object)라 한다.

 

예를 들어 불변 클래스를 만들어서 어떻게든 필드 값을 변경할 수 없게 클래스를 설계하면 된다.

  • 필드를 final로 선언
  • 값을 변경할 수 있는 메서드 제거
  • 생성자를 통해서만 값을 설정

 

※ 불변 객체를 사용하지만 그래도 값을 변경해야 하는 메서드가 필요하다면, 기존 객체의 값을 그대로 두고 대신에 변경된 결과를 새로운 객체에 담아서 반환하면 된다.

 

 

클래스를 불변으로 설계하는 이유

  • 캐시 안정성
  • 멀티 쓰레드 안정성
  • 엔티티의 값 타입