Java 공부 34일차(불변 객체란?)1편

임선구·2025년 2월 26일

몸 비틀며 Java

목록 보기
35/58

오늘의 잔디


오늘의 공부


기본형과 참조형의 공유

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

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

하나의 값을 공유하거나 또는 공유하지 않는 다는 것이 무슨 뜻인지 예제를 통해 알아보자.

기본형 예제

기본형은 하나의 값을 여러 변수에서 절대로 공유하지 않는다. 다음 예를 보자.

package lang.immutable.address;
public class PrimitiveMain {
 public static void main(String[] args) {
 //기본형은 절대로 같은 값을 공유하지 않는다.
 int a = 10;
 int b = a; // a -> b, 값 복사 후 대입
 System.out.println("a = " + a);
 System.out.println("b = " + b);
 b = 20;
 System.out.println("20 -> b");
 System.out.println("a = " + a);
 System.out.println("b = " + b); }
}

실행 결과

a = 10
b = 10
20 -> b
a = 10
b = 20

  • 기본형 변수 ab 는 절대로 하나의 값을 공유하지 않는다.
  • b = a 라고 하면 자바는 항상 값을 복사해서 대입한다. 이 경우 a 에 있는 값 10 을 복사해서 b 에 전달한다.
  • 결과적으로 ab 는 둘다 10 이라는 똑같은 숫자의 값을 가진다. 하지만 a 가 가지는 10b 가 가지는 10 은 복사된 완전히 다른 10 이다. 메모리 상에서도 a 에 속하는 10b 에 속하는 10 이 각각 별도로 존재한다.
  • b = 20 이라고 하면 b 의 값만 20 으로 변경된다.
  • a 의 값은 10 으로 그대로 유지된다.
  • 기본형 변수는 하나의 값을 절대로 공유하지 않는다. 따라서 값을 변경해도 변수 하나의 값만 변경된다. 여기서는 변수 b 의 값만 20 으로 변경되었다.

너무 당연한 이야기이다. 그렇다면 이번에는 참조형 예제를 보자.

참조형 예제

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 + '\'' +
 '}';
 }
}
  • 단순히 주소를 보관하는 객체이다.
  • 객체의 값을 편하게 확인하기 위해 IDE의 도움을 받아서 toString() 을 재정의하자
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);
 }
}
  • 처음에는 a , b 둘다 서울이라는 주소를 가져야 한다고 가정하자.
    • 따라서 Address b = a 코드를 작성했고, 변수 a , b 둘다 서울이라는 주소를 가진다.
  • 이후에 b 의 주소를 부산으로 변경한다.
  • 그런데 실행 결과를 보면 b 뿐만 아니라 a 의 주소도 함께 부산으로 변경되어 버린다.

실행 결과

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

순서대로 코드를 분석해보자.

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

  • 참조형 변수들은 같은 참조값을 통해 같은 인스턴스를 참조할 수 있다.
  • b = a 라고 하면 a 에 있는 참조값 x001 을 복사해서 b 에 전달한다.
    • 자바에서 모든 값 대입은 변수가 가지고 있는 값을 복사해서 전달한다. 변수가 int 같은 숫자값을 가지고 있으면 숫자값을 복사해서 전달하고, 참조값을 가지고 있으면 참조값을 복사해서 전달한다.
  • 참조값을 복사해서 전달하므로 결과적으로 a , b 는 같은 x001 인스턴스를 참조한다.
  • 기본형 변수는 절대로 같은 값을 공유하지 않는다.
    • 예) a=10 , b=10 과 같이 같은 모양의 숫자 10 이라는 값을 가질 수는 있지만 같은 값을 공유하는 것은 아니다. 서로 다른 숫자 10 이 두 개 있는 것이다.
  • 참조형 변수는 참조값을 통해 같은 객체(인스턴스)를 공유할 수 있다.

여기서 b 의 주소만 부산으로 변경했는데, a 의 주소도 함께 부산으로 변경되어 버린 이유는 무엇일까? 메모리 구조를 보면 바로 답이 나오겠지만, 개발을 하다 보면 누구나 이런 실수할 수 있을 것 같다는 생각도 함께 들 것이다.

공유 참조와 사이드 이펙트

사이드 이펙트(Side Effect)는 프로그래밍에서 어떤 계산이 주된 작업 외에 추가적인 부수 효과를 일으키는 것을 말한다.
앞서 b 의 값을 부산으로 변경한 코드를 다시 분석해보자.

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

  • 개발자는 b 의 주소값을 서울에서 부산으로 변경할 의도로 값 변경을 시도했다.
  • 하지만 a , b 는 같은 인스턴스를 참조한다. 따라서 a 의 값도 함께 부산으로 변경되어 버린다.

이렇게 주된 작업 외에 추가적인 부수 효과를 일으키는 것을 사이드 이펙트라 한다. 프로그래밍에서 사이드 이펙트는 보통 부정적인 의미로 사용되는데, 사이드 이펙트는 프로그램의 특정 부분에서 발생한 변경이 의도치 않게 다른 부분에 영향을 미치는 경우에 발생한다. 이로 인해 디버깅이 어려워지고 코드의 안정성이 저하될 수 있다.

사이드 이펙트 해결 방안

생각해보면 문제의 해결방안은 아주 단순하다. 다음과 같이 ab 가 처음부터 서로 다른 인스턴스를 참조하면 된다.

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

코드를 작성해보자.

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("부산");
 System.out.println("부산 -> b");
 System.out.println("a = " + a);
 System.out.println("b = " + b);
 }
}

실행 결과

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

실행 결과를 보면 b 의 주소값만 부산으로 변경된 것을 확인할 수 있다.

그림 - 생성 코드

  • ab 는 서로 다른 Address 인스턴스를 참조한다.

그림 - 변경 코드

  • ab 는 서로 다른 인스턴스를 참조한다. 따라서 b 가 참조하는 인스턴스의 값을 변경해도 a 에는 영향을 주지 않는다.

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

지금까지 발생한 모든 문제는 같은 객체(인스턴스)를 변수 a , b 가 함께 공유하기 때문에 발생했다. 따라서 객체를 공유하지 않으면 문제가 해결된다. 여기서 변수 a , b 가 서로 각각 다른 주소지로 변경할 수 있어야 한다. 이렇게 하려면 서로 다른 객체를 참조하면 된다.

객체를 공유

Address a = new Address("서울");
Address b = a;
  • 이 경우 a , b 둘다 같은 Address 인스턴스를 바라보기 때문에 한쪽의 주소만 부산으로 변경하는 것이 불가능하다.

객체를 공유 하지 않음

Address b = new Address("서울");
  • 이 경우 a , b 는 서로 다른 Address 인스턴스를 바라보기 때문에 한쪽의 주소만 부산으로 변경하는 것이 가능하다.

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

쉽게 이야기해서 여러 변수가 하나의 객체를 공유하지 않으면 지금까지 설명한 문제들이 발생하지 않는다.
그런데 여기서 문제가 있다. 하나의 객체를 여러 변수가 공유하지 않도록 강제로 막을 수 있는 방법이 없다는 것이다.
다음 예를 보자.

참조값의 공유를 막을 수 있는 방법이 없다.

Address a = new Address("서울");
Address b = a; //참조값 대입을 막을 수 있는 방법이 없다.

b = a 와 같은 코드를 작성하지 않도록 해서, 여러 변수가 하나의 참조값을 공유하지 않으면 문제가 해결될 것 같다.
하지만 Address 를 사용하는 개발자 입장에서 실수로
b = a 라고 해도 아무런 오류가 발생하지 않는다.
왜냐하면 자바 문법상 Address b = a 와 같은 참조형 변수의 대입은 아무런 문제가 없기 때문이다.

다음과 같이 새로운 객체를 참조형 변수에 대입하든, 또는 기존 객체를 참조형 변수에 대입하든, 다음 두 코드 모두 자바 문법상 정상인 코드이다.

Address b = new Address("서울") //새로운 객체 참조
Address b = a //기존 객체 공유 참조

참조값을 다른 변수에 대입하는 순간 여러 변수가 하나의 객체를 공유하게 된다. 쉽게 이야기해서 객체의 공유를 막을 수 있는 방법이 없다!

기본형은 항상 값을 복사해서 대입하기 때문에 값이 절대로 공유되지 않는다. 하지만 참조형의 경우 참조값을 복사해서
대입하기 때문에 여러 변수에서 얼마든지 같은 객체를 공유할 수 있다.
객체의 공유가 꼭 필요할 때도 있지만, 때로는 공유하는 것이 지금과 같은 사이드 이펙트를 만드는 경우도 있다.

물론 개발자가 눈을 크게 잘 뜨고! 집중에서 코드를 잘 작성하면 사이드 이펙트 문제를 일으키지 않을 수 있다. 하지만 실제로는 훨씬 더 복잡한 상황에서 이런 문제가 발생한다. 다음 코드를 보자.

package lang.immutable.address;
public class RefMain1_3 {
 public static void main(String[] args) {
 Address a = new Address("서울");
 Address b = a;
 System.out.println("a = " + a);
 System.out.println("b = " + b);
 change(b, "부산");
 System.out.println("a = " + a);
 System.out.println("b = " + b);
 }
 private static void change(Address address, String changeAddress) {
 System.out.println("주소 값을 변경합니다 -> " + changeAddress);
 address.setValue(changeAddress);
 }
}
  • 앞서 작성한 코드와 같은 코드이다. 단순히 change() 메서드만 하나 추가되었다. 그리고 change() 메서드에서 Address 인스턴스에 있는 value 값을 변경한다.
  • main() 메서드만 보면 a 의 값이 함께 부산으로 변경된 이유를 찾기가 더 어렵다.

실행 결과

a = Address{value='서울'}
b = Address{value='서울'}
주소 값을 변경합니다 -> 부산
a = Address{value='부산'}
b = Address{value='부산'}

여러 변수가 하나의 객체를 참조하는 공유 참조를 막을 수 있는 방법은 없다. 그럼 공유 참조로 인해 발생하는 문제를 어떻게 해결할 수 있을까? 단순히 개발자가 공유 참조 문제가 발생하지 않도록 조심해서 코드를 작성해야 할까?

profile
끝까지 가면 내가 다 이겨

0개의 댓글