[JAVA] 불변 객체

나의 개발 일지·2024년 12월 2일

JAVA

목록 보기
6/11

우선, 글을 작성하기 전 이 글의 모든 내용은 김영한님의 JAVA 강의를 바탕으로 함을 알립니다.

💡불변 객체

기본형과 참조형의 공유

자바의 데이터 타입은 크게 기본형(Primitive type)참조형(Reference type)으로 나뉜다.

  • 기본형

    • 데이터를 직접 변수에 저장하는 타입
    • 고정된 메모리에 할당 됨
    • 하나의 값을 여러 변수에서 공유가 불가능
    • ex. byte, int, float, boolean
    public class PrimitiveMain {
    	int a = 10;
    	int b = a;
    
    	System.out.println("a = " + a); // 10
    	System.out.println("b = " + b); // 10, a의 값이 공유된 것이 아닌 복사된 것
    
    	b = 20;
     	System.out.println("a = " + a); // 10
     	System.out.println("b = " + b); // 20

    예시 코드를 보면, 기본형으로 선언된 a,b는 데이터가 직접 저장됨을 알 수 있고 공유가 불가능함을 알 수 있다.

  • 참조형

    • 객체(인스턴스)의 주소값(참조값)을 저장하는 타입
    • 실제 데이터는 힙 메모리에 저장되며, 변수는 힙 메모리의 주소값을 참조
    • 변수에 메모리 참조값을 저장하기에 참조값을 여러 변수가 공유하여 힙 메모리에 접근이 가능
    • ex. Class, Interface, Array, Enum, String
    public class Address {
    
    	private String value;
      
     	public Address(String value) {
     		this.value = value;
     	}
      
     	public void setValue(String value) {
     		this.value = value;
     	}
      
     	public String getValue() {
     		return value;
     	}
      
     	@Override
     	public String toString() {
     		return "Address{" +
     				"value='" + value + '\'' +
    					'}';
     	}
    public class RefMain1_1 {
    	public static void main(String[] args) {
        
    		Address a = new Address("서울"); // 참조값을 x001이라고 가정하자
        	Address b = a; 
         	System.out.println("a = " + a); // a = Address{value='서울'}
         	System.out.println("b = " + b); // b = Address{value='서울'}
            
         	b.setValue("부산"); //b의 값을 부산으로 변경해야함
         	System.out.println("부산 -> b");
         	System.out.println("a = " + a); //사이드 이펙트 발생 , a = Address{value='부산'}
         	System.out.println("b = " + b); // b = Address{value='부산'}
         }
     }

    Address 클래스에 대한 객체를 생성하면 힙 메모리 영역에 객체에 대한 정보가 저장되고 참조값(x001)이 a 변수에 저장된다. 이때 Address b = a;부분에서 참조값이 복사되어 b변수에도 저장되고, 이는 a,b가 모두 같은 객체를 참조하고 있으며 같은 객체를 참조하고 있기에 a,b 모두 객체 내의 동일한 멤버를 조작 가능함을 의미한다.
    위의 코드에는 치명적인 문제가 존재한다. b.setValue("부산")을 살펴보자. 이 부분은 b를 통해 value의 값을 서울에서 부산으로 바꾸려는 시도이다. b와 a는 동일한 객체를 바라보고 있기에 b의 value만 바뀌는 것이 아닌 a의 value도 같이 바뀌는 문제가 발생하고 이를 사이드 이팩트라고 한다.

공유 참조와 사이드 이팩트

사이드 이펙트(Side Effect)는 직역하면 '부작용'을 의미한다. 프로그래밍에서 사이드 이팩트는 특정 계산이 의도한 작업 이외의 부수 효과를 발생시키는 것을 의미한다. 프로그래밍에서의 사이드 이팩트는 부정적인 의미이다. 사이드 이팩트가 발생하면 프로그램에서의 특정 부분 변경이 의도치 않은 다른 부분에서의 변경까지 초래하기 때문이다. 이로인해 유지보수와 디버깅에 어려움을 갖게된다.

참조형의 예시코드에서 발생한 사이드 이팩트 부분을 도식화하여 다시 살펴보자.

그럼 사이드 이팩트를 어떻게 해결할까?

a와 b가 각각 다른 객체를 가리키게 하면 된다.

Address a = new Address("서울");
Address b = new Address("서울");


Address 클래스에 대한 새로운 객체를 하나 더 생성하여 b가 해당 객체를 참조하도록 한다면 value값을 바꾸어도 a의 상태는 변하지 않는다. 정말 간단히 문제를 해결할 수 있다. 하지만...

좋은 코드는 '제약'을 통해 개발자의 잘못된 부분을 방지할 수 있는 것이 좋은 코드라고 영한님이 말씀해주신다.
즉, a와 b의 변수에 각각 다른 객체의 참조값을 저장하면 간단히 해결되지만 이를 강제할 방법은 없다는 것이다. 만약에 엄청나게 긴 코드를 개발하고 있고 개발자가 실수로 Address b = a;를 작성하여 사이드 이펙트가 발생했다고 가정하자. 제약이 발생하지 않아 예외처리가 불가능하다. 과연 이 부분을 지금처럼 가볍게 해결할 수 있을까? 어떻게 해야할까?

불변객체 도입

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

  • final 키워드를 통해 내부 값을 고정시킨다.
  • setter의 사용을 금지하여 외부에서 객체의 멤버 변수에 접근하지 못하도록 막는다.

위의 내용을 토대로 ImmutableAddress 클래스를 만들면 다음과 같다.

public class ImmutableAddress {
	private final String value; // final을 통해 value 필드를 고정
    
	public ImmutableAddress(String value) {
		this.value = value;
 	}
    
 	public String getValue() {
 		return value;
 	}
    
	@Override
 	public String toString() {
 		return "Address{" +
 				"value='" + value + '\'' +
 				'}';
 	}
}

ImmutableAddress의 특징은 오직 생성자를 통해서만 value 필드에 접근하여 값을 저장할 수 있고, 한번 저장된 value는 final 키워드에 의해 변경이 불가능하다는 점이다.

불변객체에서 값을 변경하려면 어떻게 해야할까?

불변 객체를 사용하지만 그래도 값을 변경 해야하는 메서드가 존재한다면 어떻게 해야할까?
기존의 값에 새로운 값을 더하는 add()가 존재하는 클래스를 만들어보자

public class ImmutableObj {

	private final int value;
    
    public MutableObj(int value) {
    	this.value = value
    }
    
    public int getValue() {
    	return value;
    }
    
    public ImmutableObj add(int addValue) {
    	int value = value + addValue;
        return new ImmutableObj(value)
    }
}

ImmutableObj 클래스의 add 메서드를 살펴보자. 메서드의 타입을 ImmutableObj 타입으로 생성하여 기존의 value와 addValue를 더한 값을 매개변수로 하는 새로운 ImmutalbleObj의 객체를 반환한다. 이렇게 메서드를 생성하면 기존의 객체의 변경없이 동일한 타입의 새로운 객체를 반환하여 새로운 결과를 만들 수 있다.

ImmutableObj obj1 = new ImmutableObj(10);
ImmutableObj obj2 = obj1.add(20);

정리

  1. 참조형인 객체의 경우 사이드 이펙트가 발생하여 특정 부분의 변경사항이 의도하지 않은 부분의 변경을 발생시킬수 있다.
  2. 사이드 이펙트를 방지하기 위해 불변객체를 사용해야한다.
    • final 키워드를 통해 필드를 하나의 값으로 고정시키기
    • setValue(setter)의 사용을 금지하여 외부에서 객체의 필드로의 직접 접근을 막기
  3. 불변객체 내에서 변경 가능한 곳이 존재하도록 하려면 해당 불변객체 타입의 메서드를 통해 새로운 객체를 반환해야한다.

불변객체가 중요한 이유가 뭘까

  • 자주 사용하는 String, 래퍼 클래스(Integer), LocalDate등 자바가 기본으로 제공하는 수많은 클래스가 불변 객체이기에 불변 객체의 기본 원리를 아는 것이 매우 중요하다.

즉, String, 래퍼 클래스, LocalDate(시간관련 클래스)는 모두 위의 불변 객체와 동일한 형태로 설계가 되어 있음을 알 수 있고 왜 해당 클래스들의 특정 메서드의 반환값을 새로운 동일한 불변 객체 타입으로 저장되어야 하는지 알 수 있을 것이다.

다음은 래퍼 클래스에 대해 알아보자

0개의 댓글