Backend/JAVA

Java 불변 객체(immutable object)

keepbang 2024. 5. 16. 17:22

 

불변 객체?

  • 객체의 상태가 변경되지 않는 객체. 변경이 불가능한 객체를 말한다.
  • Java에서는 final 키워드를 사용하여 불변 객체를 만들 수 있다.

 

불변 객체의 사용 이유

  • 객체가 변경되어도 항상 새로운 객체를 반환하기 때문에 여러 쓰레드가 동시에 한 객체에 접근해도 안전하다.
  • 디버깅이나 예측 가능성이 높아지고 메서드 실행시 사이드 이펙트를 예방할 수 있다.
  • 변경되지 말아야할 필드값이 존재할 경우 사용가능(JPA)
  • 함수형 프로그래밍으로 사용 될 수 있다.

 

코드로 알아보는 불변 객체

 

Person.class

public class Person {
    public String name;
    public Age age;

    public Person(String name, Age age) {
        this.name = name;
        this.age = age;
    }

    public Age getAge() {
        return age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

 

Age.class

public class Age {
    private int age;

    public Age(int age) {
        this.age = age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Age{" +
                "age=" + age +
                '}';
    }
}

 

Main.class

public class Main {

    public static void main(String[] args) {
        Age age = new Age(20);
        Person kim = new Person("kim", age);
        Person lee = new Person("lee", age);
        System.out.println("kim = " + kim);
        System.out.println("lee = " + lee);

        lee.getAge().setAge(21);
        System.out.println("kim = " + kim);
        System.out.println("lee = " + lee);

    }
}

 

Person클래스는 불변객체가 아니기때문에 중간에 한 객체의 나이를 변경했을때 다른 객체의 나이도 같이 변경이 된다.

 

kimlee는 같은 객체(age)를 생성자를 통해 받았다. 자바에서는 전달받은 변수의 값을 복사(call by value)해서 가져오는데 만약 객체일 경우 그 객체의 참조값을 복사하여 가져온다. 그러므로 age의 값인 20이 들어가는게 아닌 age 인스턴스 변수의 참조값이 들어가게 되고 그 참조값을 참조하게 된다. 그렇기 때문에 해당 참조값이 기르키는 곳의 값을 두 객체가 서로 공유하기 때문에 때문에 21로 변경했을 때 두 객체 모두 변경된 것이다.

이런 변경의 전파를 막고 사이드 이펙트가 발생하지 않게 하려면 어떻게 해야 할까?

 

나이를 입력할때 서로 다른 객체를 새로 생성해서 추가하면된다.

Person kim = new Person("kim", new Age(20));
Person lee = new Person("lee", new Age(20));

 

이렇게 하면 두 객체를 같은 값을 가지지만 서로 다른 참조값을 가지고 있으므로 사이드 이펙트가 발생하지 않는다.

 

하지만 개발을 하다보면 모두가 이런식으로 개발을 하는게 아니다. 그렇기 때문에 컴파일 단계에서부터 오류를 발생시켜 사이드 이펙트를 막아야한다.

 

 

이런식으로 필드값을 final로 선언하여 Age 클래스를 불변객체로 만들어주면 된다.

 

이렇게 하면 필드값 수정이 불가능하게되고 컴파일단계에서 setter를 사용할 수 없게된다. 값의 변경은 새로운 객체를 반환해 줌으로써 반환된 객체로 변경을 시도 할 수 있다.

public class ImmutableAge {
    private final int age;

    public ImmutableAge(int age) {
        this.age = age;
    }

    // 새로운 객체를 반환함.
    public ImmutableAge withAge(int newAge) {
        return new ImmutableAge(newAge);
    }

    @Override
    public String toString() {
        return "Age{" +
                "age=" + age +
                '}';
    }
}

 

public static void main(String[] args) {
        ImmutableAge immutableAge = new ImmutableAge(20);
        Person kim = new Person("kim", immutableAge);
        Person lee = new Person("lee", immutableAge);
        System.out.println("kim = " + kim);
        System.out.println("lee = " + lee);

        lee.setAge(21);
        System.out.println("kim = " + kim);
        System.out.println("lee = " + lee);
}

 

불변 객체를 사용해서 명확한 의미와 기능의 전달을 할 수 있게 되었다.

 

Person 클래스에 set 메서드를 사용하지 않고 Person 객체를 반환하는 메소드를 사용 할 수도 있다. 이럴 경우 반환된 객체를 대입하는 로직이 필요하다.

public class Person {

...

    public Person withAge(int newAge) {
        return new Person(name, new ImmutableAge(newAge));
    }
    
...
}

...

lee = lee.withAge(21);

 

 

이러한 불변 객체의 새로운 객체를 반환하는 형태는 함수형 프로그래밍에서 자주 사용된다.

 

아래는 사용자목록의 나이를 특정값으로 변경하는 로직이다.

 

// given
Person person1 = new Person("person1", new ImmutableAge(20));
Person person2 = new Person("person2", new ImmutableAge(20));
Person person3 = new Person("person3", new ImmutableAge(20));

List<Person> selectedList = List.of(person1, person2, person3);

// when
List<Person> changedList = selectedList.stream()
        .map(person -> person.withAge(21))
        .toList();

// then
System.out.println("변경 전========");
selectedList.forEach(System.out::println);

System.out.println("변경 후========");
changedList.forEach(System.out::println);

 

만약 객체를 반환하지 않는다면 map에서 새로운 객체를 생성하는 로직을 작성해야 하지만 객체를 반환하기때문에 람다식으로 작성 할 수 있게되었다.

 


 

불변 설계가 필요한 경우

 

모든 클래스를 불변으로 만든다면 안정적인 코딩이 가능하지만 코드가 복잡해 질수도있고 여러 사람들과 작업을 진행 할 대 제약사항이 많이 발생할 수 있다. 그렇기 때문에 불변 설계가 필요한지 고민해보고 필요한곳에 사용하는것이 중요하다.

'Backend > JAVA' 카테고리의 다른 글

Java 8에 추가된 것들 2  (0) 2021.09.17
Java 8에 추가된 것들 1  (0) 2021.09.09
객체지향 5원칙 (SOLID)  (0) 2021.07.28
객체지향 프로그래밍(OOP)  (0) 2021.07.27
JAVA JWT payload 가져오는 방법 / 만료시간 체크  (0) 2021.06.09