우리는 불변 객체를 사용한다. 하지만 예상치 못하게 불변을 깨트리는 악당들이 있다.
예시를 통해 상황을 이해해 보자.
public class Name {
private final String value;
public Name(String value) {
this.value = value;
}
@Override
public String toString() {
return "Name{" +
"value='" + value + '\'' +
'}';
}
}
public class Names {
private final List<Name> names;
public Names(List<Name> names) {
this.names = names;
}
public List<Name> getNames() {
return names;
}
}
Name을 가지고 있는 Names라는 일급 컬렉션이 있다. Names는 생성할 때 List <Name>를 필요로 하고, 이 값이 바뀌면 안 된다고 판단해 final을 할당해 줬다.
public class MainName {
public static void main(String[] args) {
List<Name> originalNames = new ArrayList<>();
Name name1 = new Name("pobi");
Name name2 = new Name("james");
originalNames.add(name1);
originalNames.add(name2);
Names names = new Names(originalNames);
originalNames.add(new Name("메롱메롱맨")); // 이 부분을 주의깊게 보자
for (Name name : names.getNames()) {
System.out.println(name);
}
}
}
해당 코드의 결괏값은 어떻게 될까?
Names names = new Names(originalNames); 에서 names를 만들었으니까 name1, name2 만 가지고 있는 불변 객체 아닐까?
출력 결과를 보면 메롱메롱맨이 침투한 것을 확인할 수 있다. 이런 일이 왜 발생했을까??!
디버깅을 해보니 originalNames의 참조값과 names가 가지고 있는 names의 참조값이 같은 것을 확인할 수 있었다. originalNames에 값을 더해도 두 객체 사이의 참조값이 연결되어 있으니 메롱메롱맨이 names에도 추가된 것이었다...!!
이 문제를 어떻게 해결할 수 있을까?
public class Names {
private final List<Name> names;
public Names(List<Name> names) {
this.names = new ArrayList<>(names); // 방어적 복사
}
public List<Name> getNames() {
return names;
}
}
위의 코드와 같이 Names를 생성할 때 새로운 ArrayList를 생성하고, 그곳에 names값을 저장하는 방식을 사용할 수 있다. 이러한 방법을 방어적 복사라고 한다.
디버깅을 통해 확인해 보니 originalNames의 참조값과 names가 가지고 있는 names의 참조값이 다른 것을 확인할 수 있다. 그 결과 originalNames와 names의 참조값이 끊어져 names를 불변으로 있는 것이다.
두 번째 상황을 보자.
public class Names {
private final List<Name> names;
public Names(List<Name> names) {
this.names = new ArrayList<>(names);
}
public List<Name> getNames() {
return names;
}
}
public static void main(String[] args) {
List<Name> originalNames = new ArrayList<>();
Name name1 = new Name("pobi");
Name name2 = new Name("james");
originalNames.add(name1);
originalNames.add(name2);
Names names = new Names(originalNames);
List<Name> getNames = names.getNames();
getNames.add(new Name("메롱메롱맨"));
List<Name> getNames2 = names.getNames();
for (Name name : getNames2) {
System.out.println(name);
}
}
음? 뭔가 이상하다.
우리는 Names의 값을 불변으로 선언하고 싶어 final을 할당해 불변으로 선언해 줬다. List <Name> names는 불변 객체인데 여기에 추가로 어떤 값을 넣을 수 있을까?
메롱메롱맨이 또 침투했다...!!!! 이게 무슨 상황일까? 우리는 분명 List<Name> names를 final을 통해 불변으로 선언해준거 아니였나? 왜 메롱메롱맨이 또 들어가 있을까?
final, Unmodifiable
java에서 final은 불변을 선언해 준다. 하지만 위와 같은 상황에서의 final은 단순히 객체의 재할당을 금지한다.
public class Names {
private final List<Name> names;
public Names(List<Name> names) {
this.names = new ArrayList<>(names);
this.names = new ArrayList<>(); // 컴파일 에러
}
다시 말해서, 새로운 참조값을 만들어주는 것을 금지하는 것이지 List <Name> names의 내부 값들까지도 불변으로 보장해주는 것이 아니다. 그 결과 getNames()를 통해 외부로 List<Name> names가 유출되었고, 누구든지 이 names에 새로운 이름을 추가할 수 있는 것이다.
우리는 이런 상황을 원하지 않는다. List<Name> names가 처음 만들어지면, 외부에서도 이 값을 수정하지 못하게 불변으로 사용하고 싶다. 어떻게 해야 할까?
public List<Name> getNames() {
return Collections.unmodifiableList(names);
}
이를 위해 자바에서는 불변 객체를 제공해 준다. 불변 객체를 담아서 반환해 준다면 외부에서 해당 값을 조작할 수 없다.
unmodifiable을 사용해 불변 객체를 반환한다면 메롱메롱맨은 침투할 수 없다.
public static void main(String[] args) {
List<Name> originalNames = new ArrayList<>();
Name name1 = new Name("pobi");
Name name2 = new Name("james");
originalNames.add(name1);
originalNames.add(name2);
Names names = new Names(originalNames);
List<Name> getNames = names.getNames();
getNames.add(new Name("메롱메롱맨")); // 런타임 에러
}
getNames에 add 하는 시점에서 런타임 에러가 발생한다! 이를 통해 불변을 유지할 수 있게 되었다.
추가로 생각해 볼 부분
unmodifiable을 통한 불변 객체를 가져온다는 것은 단순히 List <Name> names를 불변으로 선언한다는 것이다.
즉 names가 아닌, name이라는 내부 값은 변경이 가능하다.
다음과 같은 예시를 살펴보자. Crew 객체를 생성하고 이 안에 name, age라는 필드값이 있다.
public class Crew {
private final String name;
private int age;
public Crew(String name, int age) {
this.name = name;
this.age = age;
}
public void increaseAge() {
this.age++;
}
@Override
public String toString() {
return "Name{" +
"value='" + name + '\'' +
", age=" + age +
'}';
}
}
Name과 비슷하게 Crew의 List를 관리하는 Crews라는 일급 컬렉션이 있다.
public class Crews {
private final List<Crew> crews;
public Crews(List<Crew> crews) {
this.crews = new ArrayList<>(crews);
}
public List<Crew> getCrews() {
return Collections.unmodifiableList(crews);
}
}
public static void main(String[] args) {
List<Crew> originalCrews = new ArrayList<>();
Crew crew1 = new Crew("pobi", 20);
Crew crew2 = new Crew("james", 25);
originalCrews.add(crew1);
originalCrews.add(crew2);
Crews crews = new Crews(originalCrews);
List<Crew> getCrews = crews.getCrews();
for (Crew crew : getCrews) {
crew.increaseAge(); // 해당 메서드 호출을 통해 값을 변경할 수 있다.
}
for (Crew crew : getCrews) {
System.out.println(crew);
}
}
unmodifiable로 가져온 getCrews 자체는 불변이 맞다. 해당 리스트의 값을 변경하려면 아까와 같이 UnsupportedOperationException을 마주할 수 있다.
하지만 해당 객체가 불변인거지 내부 요소들은 변할 수 있다. 즉 crews는 불변이지만, 내부값은 crew는 불변이 아니기에 변경이 가능하다는 거다. 위와 비슷하게 setAge를 통해 불변객체의 내부값을 변경할 수도 있다.
즉 우리는 객체 내부에 있는 값들 또한 불변으로 선언해 줄 필요가 있다. 이를 리팩토링 하자면
public class Crew {
private final String name;
private final Age age;
public Crew(String name, Age age) {
this.name = name;
this.age = age;
}
public Age increaseAge() {
return new Age(age.getValue()+1);
}
다음과 같이 age 값을 포장해서 새로운 객체를 반환해 준다면 내부의 값을 안전하게 보호해 줄 수 있다. 이 방법은 새로운 객체를 반환해 준다. 즉, 과도하게 사용한다면 쓸모없는 메모리 낭비를 유발할 수 있다.
기본적으로 불변을 지향하되 어떤 데이터까지 불변으로 관리할지 잘 판단해 보자.
https://tecoble.techcourse.co.kr/post/2021-04-26-defensive-copy-vs-unmodifiable/
'java' 카테고리의 다른 글
상속과 합성 (0) | 2025.03.17 |
---|---|
Java Custom Exception (0) | 2025.03.04 |
Stream (0) | 2025.02.25 |
함수형 인터페이스 (0) | 2025.02.25 |
Thread(3) - volatile, synchronize, lock (8) | 2025.01.19 |