인프런/김영한 자바

[김영한의 실전 자바 중급 1편] - 2. 불변 객체

sson-coding 2025. 10. 10. 00:14

본 글은 김영한 님의 『김영한의 실전 자바 - 중급 1편』 강의를 학습하며 정리한 내용입니다.
강의 자료에 포함된 일부 코드와 이미지를 참고하여 발췌·활용하였습니다.

자바 기본기를 제대로 다지고 싶으시다면, 아래 링크에서 강의를 확인해 보세요
『김영한의 실전 자바 - 중급 1편』 보러 가기
본게시물은 파트너스 활동의 일환으로 작성되었으며, 구매 시 소정의 수수료를 받을 수 있습니다.

 

 

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

현재 평점 5.0점 수강생 10,673명인 강의를 만나보세요. 실무에 필요한 자바의 다양한 중급 기능을 예제 코드로 깊이있게 학습합니다. 실무에 필요한 다양한 자바 중급 기능, Object, 불변 객체, String

www.inflearn.com

 


기본형과 참조형의 공유

자바는 항상 값을 복사해서 대입하기 때문에 기본형과 참조형의 공유는 아래와 같다.

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

공유 참조와 사이드 이펙트

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

package lang.immutable.address;

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 + '\\'' +
            '}';
    }
}

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);
    }
}

b 의 값을 부산으로 변경한 코드를 분석해보자.

  1. b의 주소값을 서울에서 부산으로 변경할 의도로 값 변경을 시도했다.
  2. 하지만 a,b 는 같은 인스턴스를 참조한다.
  3. 따라서 a 의 값도 함께 부산으로 변경된다.

이렇게 주된 작업 외에 추가적인 부수 효과를 일으키는 것을 사이드 이펙트 라고 한다. 프로그래밍에서 사이드 이펙트는 부정적으로 사용되는데, 프로그램의 특정 부분에서 발생한 변경이 의도치 않게 다른 부분에 영향을 미치는 경우에 발생한다.

이로 인해 디버깅이 어려워지고 코드의 안전성이 저하될 수 있다.

사이드 이펙트 해결 방안

해결방안은 a 와 b 가 처음부터 서로 다른 인스턴스를 참조하면 된다.

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

여러 변수가 하나의 객체를 공유하는 것을 막을 방법은 없다.

위와 같은 문제는 같은 객체를 변수가 함께 공유하기 때문에 발생했다. 따라서 객체를 공유하지 않으면 문제가 해결된다.

이처럼 단순하게 서로 다른 객체를 참조해서, 같은 객체를 공유하지 않으면 문제가 해결된다.

그런데 여기서 문제가 있다. 하나의 객체를 여러 변수가 공유하지 않도록 강제로 막을 수 있는 방법이 없다는 것이다.

기본형은 항상 값을 복사해서 대입하기 때문에 값이 절대로 공유되지 않는다. 하지만 참조형의 경우 참조값을 복사해서 대입하기 때문에 여러 변수에서 얼마든지 객체를 공유할 수 있다.

객체의 공유가 꼭 필요할 때도 있지만, 때로는 공유하는 것이 지금과 같은 사이드 이펙트를 만드는 경우도 있다.

그럼 공유 참조로 인해 발생하는 문제를 어떻게 해결할 수 있을까?


불변 객체

불변 객체(Immutable Object) 는 객체의 상태가 변하지 않는것이다.

사이드 이펙트의 근본적인 원인은 공유된 객체의 값을 변경한 것에 있다. 앞서 만들었던 Address 클래스를 상태가 변하지 않는 불변 클래스로 만들어보자.

package lang.immutable.address;

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 + '\\'' +
            '}';
    }
}

특징은 다음과 같다.

  1. value 를 final 로 선언했기 때문에 내부 값이 변경이 안된다.
  2. 값을 변경할 수 있는 setValue() 를 제거했다.
  3. 이 클래스는 생성자를 통해서만 값을 설정할 수 있고, 이후에는 값을 변경하는 것이 불가능하다.

불변 클래스를 만드는 법은 위와 같이 아주 단순하다.

즉, 불변이라는 단순한 제약을 사용해서 사이드 이펙트라는 큰 문제를 막을 수 있다.

값 변경

불변 객체를 사용하지만 그래도 값을 변경해야 하는 메서드가 필요하면 어떻게 해야할까?

예를 통해 알아보자.

public class ImmutableObj {
    private final int value;
    public ImmutableObj(int value) {
        this.value = value;
    }
    public ImmutableObj add(int addValue) {
        int result = value + addValue;
        return new ImmutableObj(result);
    }
    public int getValue() {
        return value;
    }
}
package lang.immutable.change;

public class ImmutableMain1 {
    public static void main(String[] args) {
        ImmutableObj obj1 = new ImmutableObj(10);
        ImmutableObj obj2 = obj1.add(20);
        //계산 이후에도 기존값과 신규값 모두 확인 가능
        System.out.println("obj1 = " + obj1.getValue());
        System.out.println("obj2 = " + obj2.getValue());
    }
}

--실행결과--
obj1 = 10
obj2 = 30

불변 객체를 설계할 때 기존 값을 변경해야 하는 메서드가 필요할 수 있다. 이때는 기존 객체의 값을 그대로 두고 대신에 변경된 결과를 새로운 객체에 담아서 반환하면 된다. 결과를 보면 기존 값이 그대로 유지되는 것을 확인할 수 있다.

메모리 구조로 확인해보자.

  1. add(20) 을 호출한다.
  2. 기존 객체에 있는 10 과 인수로 입력한 20을 더한다. 이때 기존 객체의 값을 변경하면 안되므로 계산 결과를 기반으로 새로운 객체를 만들어서 반환한다.
  3. 새로운 객체는 x002 참조를 가진다. 새로운 객체의 참조값을 obj2 에 대입한다.

withXxx()

불변 객체에서 값을 변경하는 경우 withYear() 처럼 with 으로 시작하는 경우가 많다. 불변 객체의 메서드가 with 으로 지어진 경우, 그 메서드가 지정된 수정사항을 포함하는 객체의 새 인스턴스를 반환한다는 사실을 뜻한다.

즉, 원본 객체의 상태가 그대로 유지됨을 강조하면서 변경사항을 새 복사본에 포함하는 과정을 간결하게 표현한다.

public class ImmutableMyDate {
	private final int year;
	private final int month;
	private final int day; 
	public ImmutableMyDate(int year, int month, int day) {
		this.year = year;
		this.month = month;
		this.day = day;
	}
	public ImmutableMyDate withYear(int newYear) {
		return new ImmutableMyDate(newYear, month, day);
	}
	public ImmutableMyDate withMonth(int newMonth) {
		return new ImmutableMyDate(year, newMonth, day);
	}
	public ImmutableMyDate withDay(int newDay) {
		return new ImmutableMyDate(year, month, newDay);
	}
	@Override
	public String toString() {
			return year + "-" + month + "-" + day;
	}
}