"김영한의 실전 자바 - 중급편" 내용을 참고하여 정리함.
목차
1. 기본형과 참조형의 공유
자바의 데이터 타입은 크게 기본형 (Primitive Type)과 참조형 (Reference Type)으로 나뉜다.
- 기본형: 하나의 값을 여러 변수에서 공유하지 않음. → 값을 복사하여 대입하기 때문에 공유되지 X
- 참조형: 하나의 객체를 참조값을 통해 여러 변수에서 공유 가능. → 참조값을 공유하기 때문에 같은 인스턴스를 바라봄.
기본형 예제 (값 복사)
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
- `b = a` 실행 시 값을 복사하여 대입하므로 `b`의 변경이 `a`에 영향을 미치지 않는다.
- 메모리 상에서도 `a`에 속하는 `10`과 `b`에 속하는 `10`이 각각 별도록 존재한다.
- 기본형 변수는 하나의 값을 절대로 공유하지 않는다.
참조형 예제 (객체 공유)
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 = Address{value='서울'}
b = Address{value='서울'}
부산 -> b
a = Address{value='부산'}
b = Address{value='부산'}
- `a`의 참조값을 복사하여 전달하므로 `a`와 `b`는 같은 인스턴스를 참조한다.
- `a`와 `b`가 같은 객체를 참조하기 때문에 `b`의 값 변경이 `a`에도 영향을 준다.
- 참조형 변수는 참조값을 통해 같은 객체(인스턴스)를 공유할 수 있다.
2. 공유 참조와 사이드 이펙트
- 사이드 이펙트(Side Effect):
- 프로그래밍에서 어떤 계산이 주된 작업 외에 추가적인 부수 효과를 일으키는 것.
- 한 부분에서 발생한 변경이 의도치 않게 다른 부분에도 영향을 미치는 현상.
- 객체를 공유할 때 발생하는 대표적인 문제.
사이드 이펙트 발생 예제
Address a = new Address("서울");
Address b = a; // 같은 객체를 공유
b.setValue("부산"); // b의 값 변경
System.out.println("a = " + a); // 부산 (a도 영향을 받음) -> 사이드 이펙트 발생
System.out.println("b = " + b); // 부산
- `a`, `b`는 같은 객체(인스턴스)를 참조한다.
- `b`의 값만 변경할 의도였지만 `a`도 영향을 받는다. → 사이드 이펙트 발생
- 사이드 이펙트가 발생하면 디버깅이 어려워지고 코드의 안정성이 저하될 수 있다.
해결 방안
처음부터 서로 다른 인스턴스를 참조한다.
Address a = new Address("서울");
Address b = new Address("서울"); // 새로운 객체를 생성
- `a`와 `b`는 서로 다른 인스턴스를 참조하므로, 한 객체를 변경해도 다른 객체에 영향을 주지 않는다.
Address a = new Address("서울"); // x001
Address b = new Address("서울"); // x002
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`가 참조하는 인스턴스의 값을 변경해도 `a`에는 영향이 없다.
여러 변수가 하나의 객체를 공유하는 것을 막을 방법은 없다
앞서 본 것처럼, 객체를 공유하지 않도록 설계하면 사이드 이펙트 문제를 해결할 수 있다.
하지만 현실적으로 객체 공유를 막을 수 있는 완벽한 방법은 없다.
객체 공유를 막을 수 없는 이유
Address a = new Address("서울");
Address b = a; // 참조값을 공유 (막을 방법이 없음, 문법상 문제가 없음)
- 참조값을 대입하는 것을 문법적으로 막을 방법이 없다.
- 개발자가 실수로 `b = a`;라고 해도 자바는 이를 허용한다.
- 즉, 객체를 공유하지 않도록 강제할 방법이 없다.
3. 불변 객체 - 도입
불변 객체(Immutable Object)
- 한 번 생성되면 내부 상태가 절대 변경되지 않는 객체
- 값을 변경할 수 없으므로 객체를 공유하더라도 안전함
- 기존 객체의 값을 변경하는 대신 새로운 객체를 만들어서 반환함
불변 객체가 필요한 이유
- 공유 참조로 인한 문제 (사이드 이펙트)
- 객체를 여러 변수가 공유하면 값을 변경할 때 예기치 않은 사이드 이펙트가 발생할 수 있음.
- 공유 참조로 인해 값이 예측하지 못한 방식으로 변경될 위험이 있음.
- 객체 공유를 완전히 막을 방법이 없음
- 참조값을 대입하는 것을 문법적으로 막을 방법이 없음.
- 개발자가 실수로 b = a;라고 해도 자바는 이를 허용.
- 즉, 객체를 공유하지 않도록 강제할 방법이 없다.
=> 결론: 객체 공유를 완벽히 막을 수 없다면, 차라리 값 변경을 불가능하게 만드는 것이 더 효과적이다.
=> 해결책: 불변 객체 (Immutable Object)
불변 객체의 원리
- 불변 객체는 객체의 상태(객체 내부의 값, 필드, 멤버 변수)가 변하지 않는 객체
- 내부 필드를 final로 선언하여 변경할 수 없도록 함.
- 값을 변경하는 setter 메서드를 제공하지 않음.
- 새로운 값을 적용하려면 새로운 객체를 생성해서 반환함.
불변 객체 설계 예제
public class ImmutableAddress {
private final String value; // 필드를 final로 선언
public ImmutableAddress(String value) {
this.value = value;
}
public String getValue() {
return value;
}
@Override
public String toString() {
return "Address{" + "value='" + value + '\'' + '}';
}
}
- final 키워드 사용 → 한 번 설정된 `value` 값은 변경할 수 없음.
- Setter 메서드 제거 → `setValue()` 가 없으므로 값 변경이 불가능.
- 객체 생성 이후 상태 변경 불가 → ImmutableAddress 객체는 한 번 생성되면 절대 변경되지 않음.
=> 불변 클래스를 만드는 방법은, 객체 안에 있는 상태가 바뀌지 않도록 설게하면 된다.
불변 객체 사용 예제
// 참조형 변수는 하나의 인스턴스를 공유할 수 있다.
ImmutableAddress a = new ImmutableAddress("서울"); // x001
ImmutableAddress b = a; // 참조값 대입을 막을 수 있는 방법이 없다. // x001
System.out.println("a = " + a);
System.out.println("b = " + b);
// b.setValue("부산"); // 컴파일 오류 발생
b = new ImmutableAddress("부산"); // 새로운 객체를 생성하여 할당
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='부산'}
- `ImmutableAddress` 은 불변 객체이므로 `b` 가 참조하는 인스턴스의 값을 서울에서 부산으로 변경하려면
새로운 인스턴스를 생성해서 할당해야 한다.
불변 객체의 특징
특징 | 설명 |
값 변경 불가능 | 내부 상태가 한 번 설정되면 변경 불가 |
객체 공유 안전 | 공유되어도 값이 바뀌지 않으므로 안전 |
멀티쓰레드 안전 | 동기화 없이도 여러 쓰레드에서 안전하게 사용 가능 |
변경 시 새 객체 반환 | 기존 객체를 변경하지 않고 새로운 객체를 생성하여 반환 |
정리
- 객체를 공유하지 않더라도, 값 변경이 가능하면 사이드 이펙트가 발생할 수 있다.
- 객체를 공유하는 것을 막을 방법은 없기 때문에, 차라리 값 변경을 불가능하게 만드는 것이 효과적이다.
- 불변 객체(Immutable Object)를 사용하면 공유 참조로 인한 값 변경 문제를 근본적으로 해결할 수 있다.
- 불변 객체는 값이 변경되지 않으며, 값 변경이 필요하면 새로운 객체를 생성하여 반환한다.
참고 - 가변(Mutable) 객체 vs 불변(Immutable) 객체
- 가변: 처음 만든 이후 상태가 변할 수 있다는 뜻. (사전적으로 사물의 모양이나 성질이 달라질 수 있다는 뜻)
- 불변: 처음 만든 이후 상태가 변하지 않는다는 뜻. (사전적으로 사물의 모양이나 성질이 달라질 수 없 다는 뜻)
4. 불변 객체 - 예제
불변 객체는 객체가 생성된 후 내부 값을 변경할 수 없도록 설계된 객체다.
1. 모든 필드를 final로 선언하여 변경을 방지한다.
2. Setter 메서드를 제공하지 않는다.
3. 값이 변경되면 새로운 객체를 반환하도록 구현한다.
불변 클래스 예제: MemberV2
public class MemverV2 {
private String name;
private ImmutableAddress address; // 불변 객체 사용
public MemverV2(String name, ImmutableAddress address) {
this.name = name;
this.address = address;
}
public ImmutableAddress getAddress() {
return address;
}
public void setAddress(ImmutableAddress address) {
this.address = address;
}
@Override
public String
toString() {
return "MemverV1{" +
"name='" + name + '\'' +
", address=" + address +
'}';
}
}
- 주소를 변경할 수 없는, 불변인 `ImmutableAddress 를 사용한다.
불변 객체 사용 예제
ImmutableAddress address = new ImmutableAddress("서울");
MemverV2 memberA = new MemverV2("회원A", address);
MemverV2 memberB = new MemverV2("회원B", address);
// 회원A, 회원B의 처음 주소는 모두 서울
System.out.println("memberA = " + memberA);
System.out.println("memberB = " + memberB);
// 회원B의 주소를 부산으로 변경해야함
// memberB.getAddress().setValue("부산"); // 컴파일 오류
memberB.setAddress(new ImmutableAddress("부산")); // 새로운 객체를 생성하여 변경
System.out.println("부산 -> memberB.address");
System.out.println("memberA = " + memberA);
System.out.println("memberB = " + memberB);
실행 결과
memberA = MemverV1{name='회원A', address=Address{value='서울'}}
memberB = MemverV1{name='회원B', address=Address{value='서울'}}
부산 -> memberB.address
memberA = MemverV1{name='회원A', address=Address{value='서울'}}
memberB = MemverV1{name='회원B', address=Address{value='부산'}}
- `ImmutableAddress` 에는 값을 변경할 수 있는 메서드가 없다.
- 새로운 주소 객체를 생성하여 `setAddress` 로 전달한다.
- 결과적으로 사이드 이펙트가 발생하지 않고, 회원A는 기존 주소를 유지한다.
5. 불변 객체 - 값 변경
불변 객체는 값 변경이 불가능하지만, 새로운 값을 적용할 방법이 필요하다.
이를 위해, 기존 객체를 변경하는 것이 아니라 "새로운 객체를 만들어서 반환"해야 한다.
가변 객체(Mutable Object) 방식
public class MutableObj {
private int value;
public MutableObj(int value) {
this.value = value;
}
public void add(int addValue) { // 기존 값에 새로운 값을 더하는 메서드
value = value + addValue; // 기존 값 변경 (불변 객체 아님)
}
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
}
public class MutableMain {
public static void main(String[] args) {
MutableObj obj = new MutableObj(10);
obj.add(20);
// 계산 이후의 기존 값은 사라짐
System.out.println("obj = " + obj.getValue());
}
}
실행 결과
obj = 30
문제점
- `add()` 호출 시 기존 객체의 값이 변경된다.
- 공유된 객체라면 예상치 못한 값 변경이 발생할 위험이 있다.
- 사이드 이펙트 발생 가능히다.
불변 객체에서 값 변경하기 (새로운 객체 반환)
public class ImmutableObj {
private final int value; // final 필드 (변경 불가)
public ImmutableObj(int value) {
this.value = value;
}
public ImmutableObj add(int addValue) {
return new ImmutableObj(value + addValue); // 기존 객체 변경 없이 새로운 객체 반환
}
public int getValue() {
return value;
}
}
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

기존 객체 obj1은 변경되지 않는다.
obj2는 obj1의 값을 기반으로 새로운 객체로 생성된다.
불변 객체 방식으로 값 변경을 안전하게 처리할 수 있다.
불볍 객체 설계 시 기존 값을 변경해야 하는 메서드가 필요한 경우
- 기존 객체의 값을 그대로 두고, 변경된 결과를 새로운 객체에 담아서 반환한다.
- 기존 객체는 변경되지 않는다.
- 객체 공유 문제가 없다. (사이드 이펙즈 방지)
참고 - withXxx()
불변 객체에서 값을 변경하는 경우, 메서드 이름이 withXxx() 형태로 자주 사용된다.
"with"의 의미
- "coffee with sugar" → 커피에 설탕이 추가되어 원래의 상태를 변경하여 새로운 변형을 만든다는 것을 의미
- 프로그래밍에서도 "with"는 원본 객체를 유지한 채 일부 변경사항을 반영한 새 객체(인스턴스)를 반환하는 의미
불변 객체에서 "with" 사용 예시
LocalDate date = LocalDate.of(2024, 3, 4);
LocalDate newDate = date.withYear(2025); // 새로운 객체 반환
System.out.println(date); // 2024-03-04
System.out.println(newDate); // 2025-03-04
- `date` 객체는 변경되지 않고, `newDate` 라는 새로운 객체가 반환됨
- `withYear(2025)` 는 "연도를 2025로 변경한 새로운 LocalDate 객체를 반환"
"with"는 불변 객체에서 값을 변경할 때 새로운 객체를 반환하는 메서드 명명 규칙으로 자주 사용된다.
원본 객체는 변경되지 않는다는 점을 강조하면서, 새로운 객체를 생성하는 과정을 직관적으로 표현한다.
6. 정리
불변 객체를 학습한 이유
- 자바에서 가장 많이 사용되는 String 클래스가 대표적인 불변 객체(Immutable Object)
- 뿐만 아니라 Integer, LocalDate 등 자바 기본 제공 클래스 중 다수가 불변 객체로 설계됨
- 따라서 불변 객체의 필요성과 원리를 이해해야 자바 기본 클래스를 더 깊이 있게 이해할 수 있음
모든 클래스를 불변 객체로 만들지는 않는다
- 우리가 개발하는 대부분의 클래스는 값을 변경할 수 있는 가변 객체(Mutable Object)로 설계됨
- 예: 회원 정보 관리 클래스 → 회원의 속성(이름, 주소 등)은 변경될 수 있어야 함
- 불변 객체는 "값이 변경되면 안 되는 특별한 경우"에 만들어서 사용
- 때로는 같은 기능을 하는 클래스를 하나는 불변, 하나는 가변으로 각각 만드는 경우도 있음
불변 객체의 주요 장점
- 캐시(Cache) 안정성
- 멀티 쓰레드(Thread) 환경에서 안전하게 사용 가능
- 엔티티의 값 타입을 안정적으로 유지할 수 있음
'Course > Java' 카테고리의 다른 글
[java-mid1] 1. Object 클래스 (0) | 2025.02.25 |
---|---|
[java-basic] 12. 다형성과 설계 (1) | 2025.02.24 |
[java-basic] 11. 다형성2 (0) | 2025.01.30 |
[java-basic] 10. 다형성1 (0) | 2025.01.20 |
[java-basic] 9. 상속 (0) | 2024.12.27 |
댓글