불변 객체의 개념과 장점, 불변 객체를 사용하는 방법을 소개합니다.
❏ 불변 객체?
불변 객체(Immutable Object)란 객체 생성 이후에는 객체의 상태가 바뀌지 않는 객체를 의미한다.
❍ 불변 객체의 장점
1️⃣ 객체의 대한 신뢰도가 높아지고, 안전하게 서비스 개발할 수 있다.
예를 들어, Pet이라는 객체가 name, age 등등의 필드를 가진 상황이다.
...
if (healthService.isBestRunner(pet)) {
healthService.increasePoint(pet);
}
if (healthService.isNotHealthy(pet) {
healthService.runTraining(pet);
}
...
pet이 Best Runner라면 increasePoint( )를 실행하고, isNotHealthy하다면 runTraining( )을 실행한다고 해보자. pet이 가변 객체일 경우엔 increasePoint( )와 runTraining( )을 통해 pet의 상태가 변화되지는 않았을까?하는 의문이 들게 된다.
그렇다면 상태가 변화되지는 않았는지 다시 한 번 확인을 해주는 등의 번거로움이 추가될 수 있다. pet이 불변 객체일 경우에는 걱정할 필요 없이 상태의 변화가 없음을 확신할 수 있게 된다.
또한, 실수로 상태를 변화시키더라도 반영되지 않기 때문에 안정적으로 서비스 개발을 할 수 있다.
2️⃣ Thread-Safe하여 병렬 프로그래밍에 유용하다.
멀티 쓰레드 환경에서의 동시에 쓰여지는 동기화 문제가 발생하지 않게 된다. 불변 객체는 항상 동일한 상태이므로, 동기화를 고려하지 않아도 되며, 성능상의 이점도 가질 수 있다.
3️⃣ 불변 객체를 필드로 사용하면, 방어적 복사를 할 필요가 없다.
❏ String
String은 자바에서 기본적으로 제공하는 대표적인 불변 객체이다.
String code1 = "안녕하세요";
String code2 = code1;
System.out.println(code1 == code2); // true
code1과 code2는 메모리 상에서 같은 값인 “안녕하세요”를 가리키므로, ==를 통해 비교하면 같은 객체를 지니고 있으니 true가 출력될 것이다.
code1 = "hello";
System.out.println(code1 == code2); // false
그러나, code1이 변경된다면 false가 된다. 이유는 code1은 “hello”를 가리키게 되고, code2는 여전히 “안녕하세요”를 가리키고 있기 때문이다. 이처럼 String은 값이 변경되지 않는 불변 객체이다.
❏ 불변 객체 만들기
public class Person {
private String name;
private int age;
public Person(String name, int age){
this.name = name;
this.age = age;
}
public String getName(){
return name;
}
public void setName(String name){
this.name = name;
}
public String getAge(){
return age;
}
public void setAge(int age){
this.age= age;
}
}
이와 같은 Person 클래스가 존재한다고 하자. 불변 객체로 만들려면 어떻게 해야 할까?
1️⃣상태가 변경될 만한 요소를 모두 없애주어야 한다.
public class Person {
private String name;
private int age;
public Person(String name, int age){
this.name = name;
this.age = age;
}
public String getName(){
return name;
}
public String getAge(){
return age;
}
}
setName과 setAge 메서드를 삭제해주었다.
2️⃣ 모든 필드에 final 키워드를 사용해야 한다.
public class Person {
private final String name;
private final int age;
public Person(String name, int age){
this.name = name;
this.age = age;
}
public String getName(){
return name;
}
public String getAge(){
return age;
}
}
final 키워드를 사용해줌으로써, 상태가 변경되지 않도록 한다.
3️⃣ 상속을 방지한다.
class NewPerson extends Person {
private String newName;
...
public void setName(String name){
this.newName = name;
}
public String getName(){
return this.newName;
}
}
Person person = new NewPerson("person1");
System.out.println(person.getName()); // person1
NewPerson newPerson = (NewPerson) person;
newPerson.setName("person2");
System.out.println(person.getName()); // person2
이처럼 메서드 override로 인해 person의 상태가 변화될 수 있다. 따라서, 상속 자체를 방지하여 이러한 상태 변화를 방지하여야 한다.
public final class Person {
private final String name;
private final int age;
public Person(String name, int age){
this.name = name;
this.age = age;
}
public String getName(){
return name;
}
public String getAge(){
return age;
}
}
class 앞에 final 키워드를 붙여주어 방지할 수 있다.
4️⃣ 불변 객체가 가진 필드 중 가변 객체가 있다면, 새로운 객체로 만들어 가변 개체를 공유해서 사용하지 않아야 한다. 방어적 복사를 활용하자.
public final class Person {
private final String name;
private final int age;
private final Home home;
public Person(String name, int age, Home home){
this.name = name;
this.age = age;
// this.home = home; !! home의 메서드를 통해 상태 변화될 수 있음
this.home = new Home(home.number);
}
public String getName(){
return name;
}
public String getAge(){
return age;
}
public Home getHome(){
// return home; !! 마찬가지로 home의 메서드를 통해 상태변화될 수 있음
return new Home(home.number);
}
}
5️⃣ 애초에 불변 객체가 가진 필드에 가변 객체를 사용하지 않고, 불변 객체를 사용한다면, 방어적 복사를 해줄 필요가 없다.
이처럼 장점이 분명 존재하는 불변 객체이지만, 새로운 객체를 활용해야 하므로 자원 소모가 많다라는 우려도 있으나, Oracle 공식 문서에 따르면, 객체를 잘 활용하면 매번 새로운 할당을 방지할 수 있으므로 장점이 더 많은 불변 객체의 사용을 권장한다고 한다.
참고)