Computer >> 컴퓨터 >  >> 프로그래밍 >> Java

Java에서 가변 객체 참조가 포함된 불변(Immutable) 클래스 만드는 방법

불변(Immutable) 객체란 한 번 초기화되면 상태를 변경할 수 없는 객체를 의미합니다. 요구 사항에 따라 불변 클래스를 직접 만들어야 하는 경우가 종종 있습니다. 예를 들어 Java의 모든 기본 타입 래퍼 클래스(Integer, Byte, Long, Float, Double, Character, Boolean, Short)는 불변이며, String 클래스 역시 대표적인 불변 클래스입니다.

커스텀 불변 클래스를 만드는 방법

가변(mutable) 객체 참조를 포함하더라도 완전한 불변성을 유지하려면 다음 규칙들을 지켜야 합니다.

  • 클래스를 final로 선언하여 상속으로 인한 변형을 차단합니다.
  • 모든 필드를 private으로 선언해 외부에서 직접 접근하지 못하도록 합니다.
  • 필드 값을 수정하는 setter 메서드를 제공하지 않습니다.
  • 모든 가변 필드를 final로 선언하여 값이 한 번만 할당되도록 합니다.
  • 생성자에서 깊은 복사(deep copy)를 수행하며 모든 필드를 초기화합니다.
  • getter 메서드에서 원본 객체 대신 복제(clone)된 복사본을 반환합니다.
  • 인스턴스 필드에 가변 객체에 대한 참조가 포함된 경우, 해당 객체가 외부에서 변경되지 않도록 보호합니다.
  • 내부의 가변 객체를 수정하는 메서드를 노출하지 않습니다.
  • 생성자로 전달된 외부 가변 객체의 참조를 그대로 저장하지 않고, 필요 시 복사본을 생성해 저장합니다. 마찬가지로 내부 가변 객체를 반환할 때도 원본이 아닌 복사본을 반환해야 합니다.

예제 코드

// Employee.java
final class Employee {
    private final String empName;
    private final int age;
    private final Address address;

    public Employee(String name, int age, Address address) {
        super();
        this.empName = name;
        this.age = age;
        this.address = address;
    }

    public String getEmpName() {
        return empName;
    }

    public int getAge() {
        return age;
    }

    /* 원본 참조를 그대로 반환하면 불변성이 깨지므로 사용하지 않음
    public Address getAddress() {
        return address;
    }
    */

    public Address getAddress() throws CloneNotSupportedException {
        return (Address) address.clone();
    }
}

// Address.java
class Address implements Cloneable {
    public String addressType;
    public String address;
    public String city;

    public Address(String addressType, String address, String city) {
        super();
        this.addressType = addressType;
        this.address = address;
        this.city = city;
    }

    public String getAddressType() {
        return addressType;
    }

    public void setAddressType(String addressType) {
        this.addressType = addressType;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    public String getCity() {
        return city;
    }

    public void setCity(String city) {
        this.city = city;
    }

    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    @Override
    public String toString() {
        return "Address Type - " + addressType + ", address - " + address + ", city - " + city;
    }
}

// MainClass.java
public class MainClass {
    public static void main(String[] args) throws Exception {
        Employee emp = new Employee("Adithya", 34,
                new Address("Home", "Madhapur", "Hyderabad"));

        Address address = emp.getAddress();
        System.out.println(address);

        // getter로 받은 복사본을 수정해도 원본에는 영향이 없음
        address.setAddress("Hi-tech City");
        address.setAddressType("Office");
        address.setCity("Hyderabad");

        System.out.println(emp.getAddress());
    }
}

위 예제에서 getter는 원본 Address 객체 대신 해당 인스턴스의 깊게 복제된 복사본을 반환합니다. 이를 위해 Address 클래스는 반드시 Cloneable 인터페이스를 구현해야 합니다. 덕분에 외부에서 getter로 얻은 복사본의 값을 아무리 변경해도 Employee 객체 내부의 주소 정보는 변하지 않으며, 불변성이 온전히 유지됩니다.

실행 결과

Address Type - Home, address - Madhapur, city - Hyderabad
Address Type - Home, address - Madhapur, city - Hyderabad