김영한의 실전 자바 - 중급 1편 강의 | 김영한 - 인프런
김영한 | , 국내 개발 분야 누적 수강생 1위, 제대로 만든 김영한의 실전 자바[사진][임베딩 영상]단순히 자바 문법을 안다? 이걸로는 안됩니다!전 우아한형제들 기술이사, 누적 수강생 40만 명 돌
www.inflearn.com
기본형과 참조형의 공유
자바의 데이터 타입은 가장 크게 보면 기본형(Primitive Type)과 참조형(Reference Type)으로 나눌 수 있다.
- 기본형: 하나의 값을 여러 변수에서 절대 공유하지 않는다.
- 참조형: 하나의 객체를 참조값을 통해 여러 변수에서 공유할 수 있다.
기본형의 경우
기본형은 하나의 값을 여러 변수에서 절대로 공유하지 않는다.
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
- 기본형 변수 `a`와 `b`는 절대로 하나의 값을 공유하지 않는다.
- `b=a`라고 하면 자바는 항상 값을 복사해서 대입한다. 이 경우 `a`에 있는 값 `10`을 복사해서 `b`에 전달한다.
- 결과적으로 `a`와 `b`는 둘 다 `10`이라는 똑같은 숫자의 값을 가진다. 하지만 `a`가 가지는 `10`과 `b`가 가지는 `10`은 복사된 완전히 다른 `10`이다. 메모리 상에서도 `a`에 속하는 `10`과 `b`에 속하는 `10`이 각각 별도로 존재한다.
- `b=10`이라고 하면 `b`의 값만 `20`으로 변경된다.
- `a`의 값은 `10`으로 그대로 유지된다.
- 기본형 변수는 하나의 값을 절대로 공유하지 않는다. 따라서 값을 변경해도 변수 하나의 값만 변경된다. 여기서는 변수 `b`의 값만 `20`으로 변경되었다.
참조형의 경우
public class Address {
private String value;
public Address(String value) {
this.value = value;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
@Override
public String toString() {
return "Address{" +
"value='" + value + '\'' +
'}';
}
}
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` 둘 다 서울이라는 주소를 가졌다.
- 이후에 `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=10`, `b=10`과 같이 같은 모양의 숫자 `10`이라는 값을 가질 수는 있지만 같은 값을 공유하는 것은 안디ㅏ. 서로 다른 숫자 `10`이 두 개 있는 것이다.
- 참조형 변수는 참조값을 통해 같은 객체(인스턴스)를 공유할 수 있다.
공유 참조와 사이드 이펙트
사이드 이펙트(Side Effect)는 프로그래밍에서 어떤 계산이 주된 작업 외에 추가적인 부수 효과를 일으키는 것을 말한다.
b.setValue("부산"); //b의 값을 부산으로 변경해야함
System.out.println("부산 -> b");
System.out.println("a = " + a); //사이드 이펙트 발생
System.out.println("b = " + b);
- 개발자는 `b`의 주소값을 서울에서 부산으로 변경할 의도로 값 변경을 시도했다.
- 하지만 `a`, `b`는 같은 인스턴스를 참조한다. 따라서 `a`의 값도 함께 부산으로 변경되어 버린다.
이렇게 주된 작업 외에 추가적인 부수 효과를 일으키는 것을 사이드 이펙트라 한다. 프로그래밍에서 사이드 이펙트는 보통 부정적인 의미로 사용되는데, 사이드 이펙트는 프로그램의 특정 부분에서 발생한 변경이 의도치 않게 다른 부분에 영향을 미치는 경우에 발생한다. 이로 인해 디버깅이 어려워지고 코드의 안정성이 저하될 수 있다.
사이드 이펙트 해결 방안
해결방안은 아주 단순하다. 다음과 같이 `a`와 `b`가 처음부터 서로 다른 인스턴스를 참조하면 된다.
Address a = new Address("서울");
Address b = new 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`의 주소값만 부산으로 변경된 것을 알 수 있다.
`a`와 `b`는 서로 다른 인스턴스를 참조한다. 따라서 `b`가 참조하는 인스턴스의 값을 변경해도 `a`에 영향을 주지 않는다.
여러 변수가 하나의 객체를 공유하는 것을 막을 방법은 없다.
단순하게 서로 다른 객체를 참조해서, 같은 객체를 공유하지 않으면 문제가 해결된다. 하지만 하나의 객체를 여러 변수가 공유하지 않도록 강제로 막을 수 있는 방법이 없다.
다음과 같이 새로운 객체를 참조형 변수에 대입하든, 또는 기존 객체를 참조형 변수에 대입하든, 다음 두 코드 모두 자바 문법상 정상인 코드이다.
Address b = new Address("서울") //새로운 객체 참조
Address b = a //기존 객체 공유 참조
불변 객체
사이드 이펙트의 더 근본적인 원인을 고려해보면, 객체를 공유하는 것 자체는 문제가 아니다. 객체를 공유한다고 바로 사이드 이펙트가 발생하지는 않는다. 문제의 직접적인 원인은 공유된 객체의 값을 변경한 것에 있다.
처음에는 `b = a`와 같이 `"서울"`이라는 `Address` 인스턴스를 `a`, `b`가 함께 사용하는 것이 서로 다른 인스턴스를 사용하는 것보다 메모리와 성능상 더 효율적이다. 인스턴스가 하나이니 메모리가 절약되고, 인스턴스를 하나 생성하지 않아도 되니 생성 시간이 줄어서 성능상 효율적이다.
Address a = new Address("서울");
Address b = a;
진짜 문제는 이후에 b가 공유 참조하는 인스턴스의 값을 변경하기 때문에 발생한다.
b.setValue("부산"); //b의 값을 부산으로 변경해야함
System.out.println("부산 -> b");
System.out.println("a = " + a); //사이드 이펙트 발생
System.out.println("b = " + b);
자바에서 여러 참조형 변수가 하나의 객체(인스턴스)를 참조하는 공유 참조 문제는 피할 수 없다. 기본형과 다르게 참조형인 객체는 처음부터 여러 참조형 변수에서 공유될 수 있도록 설계되었다. 따라서 이것은 문제가 아니다.
문제의 직접적인 원인은 공유될 수 있는 `Address` 객체의 값을 어디선가 변경했기 떄문이다.
만약 `Address` 객체의 값을 변경하지 못하게 설계했다면 이런 사이드 이펙트 자체가 발생하지 않을 것이다.
도입
객체의 상태(객체 내부의 값, 필드, 멤버 변수)가 변하지 않는 객체를 불변 객체(Immutable Object)라 한다. 앞서 만들었던 `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 + '\'' +
'}';
}
}
- 내부 값이 변경되면 안된다. 따라서 `value`의 필드를 `final`로 선언했다.
- 값을 변경할 수 있는 `setValue()`를 제거했다.
- 이 클래스는 생성자를 통해서만 값을 설정할 수 있고, 이후에는 값을 변경하는 것이 불가능하다.
public class RefMain2 {
public static void main(String[] args) {
ImmutableAddress a = new ImmutableAddress("서울");
ImmutableAddress b = a; //참조값 대입을 막을 수 있는 방법이 없다.
System.out.println("a = " + a);
System.out.println("b = " + b);
b = new ImmutableAddress("부산");
System.out.println("부산 -> b");
System.out.println("a = " + a);
System.out.println("b = " + b);
}
}
- `b.setValue()` 메서드 자체가 제거되었다.
- 이제 `ImmutableAddress` 인스턴스의 값을 변경할 수 있는 방법은 없다.
- `ImmutableAddress`를 사용하는 개발자는 값을 변경하려고 시도하다가, 값을 변경하는 것이 불가능하다는 사실을 알고, 이 객체가 불변 객체인 사실을 깨닫게 된다.
- 따라서 어쩔 수 없이 새로운 `ImmutableAddress("부산")` 인스턴스를 생성해서 `b`에 대입한다.
- 결과적으로 `a`, `b`는 서로 다른 인스턴스를 참조하고, `a`가 참조하던 `ImmutableAddress`는 그대로 유지된다.
가변(Mutable) 객체 vs 불변(Immutable) 객체
가변은 이름 그대로 처음 만든 이후 상태가 변할 수 있다는 뜻이다. (사전적으로 사물의 모양이나 성질이 달라질 수 있다는 뜻이다.) 불변은 이름 그대로 처음 만든 이후 상태가 변하지 않는다는 뜻이다. (사전적으로 사물의 모양이나 성질이 달라질 수 없다는 뜻이다.)
예시
public class MemberV2 {
private String name;
private ImmutableAddress address;
public MemberV2(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 "MemberV1{" +
"name='" + name + '\'' +
", address=" + address +
'}';
}
}
- `MemberV2`는 주소를 변경할 수 없는, 불변인 `ImmutableAddress`를 사용한다.
public class MemberMainV2 {
public static void main(String[] args) {
ImmutableAddress address = new ImmutableAddress("서울");
MemberV2 memberA = new MemberV2("회원A", address);
MemberV2 memberB = new MemberV2("회원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);
}
}
- `회원B`의 주소를 중간에 부산으로 변경하려고 시도한다. 하지만 `ImmutableAddress`에는 값을 변경할 수 있는 메서드가 없다. 따라서 컴파일 오류가 발생한다.
- 결국 `memberB.setAddress(new ImmutableAddress("부산"))`와 같이 새로운 주소 객체를 만들어서 전달한다.
값 변경
불변 객체를 사용하지만 그래도 값을 변경해야 하는 메서드가 있으면 어떻게 해야할까?
예를 들어서 기존 값에 새로운 값을 더하는 `add()`와 같은 메서드가 있다.
먼저 변경 가능한 객체에서 값을 변경하는 간단한 예를 만들어보자.
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
- `MutableObj`을 `10`이라는 값으로 생성한다.
- 이후에 `obj.add(20)`을 통해서 `10 + 20`을 수행한다.
- 계산 이후에 기존에 있던 `10`이라는 값은 사라진다.
- `MutableObj`의 상태(값)가 `10` `30`으로 변경된다.
- `obj.getValue()`를 호출하면 `30`이 출력된다.
이번에는 불변 객체에서 `add()` 메서드를 어떻게 구현하는지 알아보자.
참고로 불변 객체는 변하지 않아야 한다.
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;
}
}
- 여기서 핵심은 `add()` 메서드이다.
- 불변 객체는 값을 변경하면 안된다. 그러면 이미 불변 객체가 아니다.
- 하지만 여기서는 기존 값에 새로운 값을 더해야 한다.
- 불변 객체는 기존 값을 변경하지 않고 대신에 새로운 계산 결과를 바탕으로 새로운 객체를 만들어서 반환한다. 이렇게 하면 불변도 유지하면서 새로운 결과도 만들 수 있다.
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
불변 객체를 설계할 때 기존 값을 변경해야 하는 메서드가 필요할 수 있다. 이때는 기존 객체의 값을 그대로 두고 대신에 변경된 결과를 새로운 객체에 담아서 반환하면 된다. 결과를 보면 기존 값이 그대로 유지되는 것을 확인할 수 있다.
- `add(20)`을 호출한다.
- 기존 객체에 있는 `10`과 인수로 입력한 `20`을 더한다. 이때 기존 객체의 값을 변경하면 안되므로 계산 결과를 기반으로 새로운 객체를 만들어서 반환한다.
- 새로운 객체는 `x002` 참조를 가진다. 새로운 객체의 참조값은 `obj2`에 대입한다.
새로 생성된 반환 값을 사용하지 않는 경우
public class ImmutableMain2 {
public static void main(String[] args) {
ImmutableObj obj1 = new ImmutableObj(10);
obj1.add(20);
System.out.println("obj1 = " + obj1.getValue());
}
}
실행 결과
obj1 = 10
불변 객체에서 변경과 관련된 메서드들은 보통 객체를 새로 만들어서 반환하기 때문에 꼭 반환 값을 받아야 한다.
withXxx()
불변 객체에서 값을 변경하는 경우 `withYear()`처럼 "with"으로 시작하는 경우가 많다.
예를 들어 "coffee with sugar"라고 하면, 커피에 설탕이 추가되어 원래의 상태를 변경하여 새로운 변형을 만든다는 것을 의미한다.
이 개념을 프로그래밍에 적용하면, 불변 객체의 메서드가 "with"으로 이름 지어진 경우, 그 메서드가 지정된 수정사항을 포함하는 객체의 새 인스턴스를 반환한다는 사실을 뜻한다.
정리하면 "with"는 관례처럼 사용되는데, 원본 객체의 상태가 그대로 유지됨을 강조하면서 변경사항을 새 복사본에 포함하는 과정을 간결하게 표현한다.
'Java' 카테고리의 다른 글
[Java/김영한] 래퍼, Class 클래스 (0) | 2025.03.23 |
---|---|
[Java/김영한] String 클래스 (0) | 2025.03.22 |
[Java/김영한] Object 클래스 (0) | 2025.03.04 |
[Java/김영한] 다형성 (1) | 2025.03.03 |
[Java/김영한] 상속 (1) | 2025.02.28 |