아이템 17. 변경 가능성을 최소화 하라

weekbelt·2022년 11월 5일
0

1. 불변 클래스

  • 불변 클래스는 가변 클래스보다 설계하고 구현하고 사용하기 쉬우며, 오류가 생길 여지도 적고 훨씬 안전한다.

불변 클래스를 만드는 다섯가지 규칙

1. 객체의 상태를 변경하는 메서드를 제공하지 않는다.

public class PhoneNumber {

    private short areaCode, prefix, lineNum;

    public PhoneNumber(short areaCode, short prefix, short lineNum) {
        this.areaCode = areaCode;
        this.prefix = prefix;
        this.lineNum = lineNum;
    }

    public short getAreaCode() {
        return areaCode;
    }

    public short getPrefix() {
        return prefix;
    }

    public short getLineNum() {
        return lineNum;
    }
}

생성자로 초기값을 설정하고 setter와 같은 객체상태를 변경할 수 있는 메서드를 제공하지 않습니다.

2. 클래스를 확장할 수 없도록 한다.

첫번째 규칙으로 객체 상태를 변경할 수는 없지만 만약 PhoneNumber클래스를 상속받는 객체의 상태값을 변경하는 메소드가 제공됐을때는 불변클래스가 아니게 됩니다. 따라서 클래스를 final로 선언하거나 생성자를 private으로 생성하여 상속을 받을 수 없게 합니다.

final클래스로 선언
public final class PhoneNumber {

    private short areaCode, prefix, lineNum;

    public PhoneNumber(short areaCode, short prefix, short lineNum) {
        this.areaCode = areaCode;
        this.prefix = prefix;
        this.lineNum = lineNum;
    }

	// getter
}
private 생성자로 선언
public class PhoneNumber {

    private short areaCode, prefix, lineNum;

    private PhoneNumber(short areaCode, short prefix, short lineNum) {
        this.areaCode = areaCode;
        this.prefix = prefix;
        this.lineNum = lineNum;
    }

	// getter
}

private생성자로 선언한 경우는 초기값을 설정할 수 있는 정적팩토리 메서드를 제공해야합니다.

모든 필드를 final로 선언한다.

public class PhoneNumber {

    private final short areaCode, prefix, lineNum;	// final로 선언

    public PhoneNumber(short areaCode, short prefix, short lineNum) {
        this.areaCode = areaCode;
        this.prefix = prefix;
        this.lineNum = lineNum;
    }
    
    public void doSomething() {	// 이렇게 내부 상태를 변경하는 경우를 방지
    	this.areaCode = 10;
    }

	// getter
}

필드를 final로 선언하면 doSomething메서드와 같이 내부 필드상태를 변경할 경우 컴파일에러가 발생하기 때문에 컴파일시 상태변경을 확인할 수 있어 객체 상태를 변경하는 실수를 방지할 수 있습니다.

모든 필드를 private으로 선언한다.

public class PhoneNumber {

    public final short areaCode, prefix, lineNum;	// 외부에 노출

    public PhoneNumber(short areaCode, short prefix, short lineNum) {
        this.areaCode = areaCode;
        this.prefix = prefix;
        this.lineNum = lineNum;
    }

	// getter
}

외부에서 PhoneNumber의 필드들을 노출하기때문에 모든 필드들을 private으로 선언합니다.

자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다.

Address 가변 클래스
public class Address {

    private String zipCode;

    private String street;

    private String city;

    public String getZipCode() {
        return zipCode;
    }

    public void setZipCode(String zipCode) {
        this.zipCode = zipCode;
    }

    public String getStreet() {
        return street;
    }

    public void setStreet(String street) {
        this.street = street;
    }

    public String getCity() {
        return city;
    }

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

setter를 통해서 Address의 상태를 변경할 수 있는 가변 클래스입니다.

Person 불변 클래스
public final class Person {

    private final Address address;

    public Person(Address address) {
        this.address = address;
    }

    public Address getAddress() {
		return address;
    }
}

Person클래스는 불변클래스인것 처럼 보이지만 Address클래스를 참조하는데 이 Address클래스가 가변클래스이기 때문에 Person객체가 참조하는 Address객체의 정보가 얼마든지 빠뀔수 있습니다.

Person객체가 참조하는 Address의 상태를 변경
        Address seattle = new Address();
        seattle.setCity("Seattle");

        Person person = new Person(seattle);

        Address redmond = person.getAddress();
        redmond.setCity("Redmond");
public final class Person {

    private final Address address;

    public Person(Address address) {
        this.address = address;
    }

    public Address getAddress() {	// 가변 객체에 접근할 수 없게 제거
		return address;
    }
}

결국 Person이 가지고 있는 정보가 변경되기때문에 불변성이 깨지게 됩니다. 따라서 가변적인 객체를 접근할 수 없게 막아야합니다.

public final class Person {

    private final Address address;

    public Person(Address address) {
        this.address = address;
    }

    public Address getAddress() {
        Address copyOfAddress = new Address();
        copyOfAddress.setStreet(address.getStreet());
        copyOfAddress.setZipCode(address.getZipCode());
        copyOfAddress.setCity(address.getCity());
        return copyOfAddress;
    }
}

만약 Person객체의 Address의 정보를 제공해야한다면 Person객체가 참조하고 있는 가변객체를 직접 리턴하지 말고 새 인스턴스에 방어적복사로 리턴하도록 하면 Person객체가 참조하고 있는 Address객체의 정보를 변경할 수 없습니다.

2. 불변클래스의 장점과 단점

장점

코드 17-1 불변 복소수 클래스 (106-107쪽)
public final class Complex {
    private final double re;
    private final double im;

    public static final Complex ZERO = new Complex(0, 0);
    public static final Complex ONE  = new Complex(1, 0);
    public static final Complex I    = new Complex(0, 1);

    public Complex(double re, double im) {
        this.re = re;
        this.im = im;
    }

    public double realPart()      { return re; }
    public double imaginaryPart() { return im; }

    public Complex plus(Complex c) {
        return new Complex(re + c.re, im + c.im);
    }

    // 코드 17-2 정적 팩터리(private 생성자와 함께 사용해야 한다.) (110-111쪽)
    public static Complex valueOf(double re, double im) {
        return new Complex(re, im);
    }

    public Complex minus(Complex c) {
        return new Complex(re - c.re, im - c.im);
    }

    public Complex times(Complex c) {
        return new Complex(re * c.re - im * c.im,
                re * c.im + im * c.re);
    }

    public Complex dividedBy(Complex c) {
        double tmp = c.re * c.re + c.im * c.im;
        return new Complex((re * c.re + im * c.im) / tmp,
                (im * c.re - re * c.im) / tmp);
    }
}

1. 함수형 프로그래밍에 적합하다. (피연산자에 함수를 적용한 결과를 반환하지만 피연산자가 바뀌지는 않는다.)

    public Complex plus(Complex c) {
        return new Complex(re + c.re, im + c.im);
    }

Complex클래스의 덧셈연산의 경우 현재 Complex의 값이 바뀌는게 아니라 새로운 Complex객체가 생성되도록 하여 값이 바뀌지가 않습니다.

2. 불변 객체는 단순하다.

3. 불변 객체는 근본적으로 스레드 안전하여 따로 동기화할 필요가 없다.

4. 불변 객체는 안심하고 공유할 수 있다. (상수, public static find)

5. 불변 객체 끼리는 내부 데이터를 공유할 수 있다.

6. 객체를 만들 때 불변 객체로 구성하면 이점이 많다.

7. 실패 원자성을 제공한다.

단점

1. 값이 다르다면 반드시 별도의 객체로 만들어야 한다.

3. 불변 클래스를 만들 때 고려할 것

package me.whiteship.chapter04.item17.part3;

// 코드 17-1 불변 복소수 클래스 (106-107쪽)
public class Complex {
    private final double re;
    private final double im;

    public static final Complex ZERO = new Complex(0, 0);
    public static final Complex ONE  = new Complex(1, 0);
    public static final Complex I    = new Complex(0, 1);

    private Complex(double re, double im) {
        this.re = re;
        this.im = im;
    }

    private static class MyComplex extends Complex {

        private MyComplex(double re, double im) {
            super(re, im);
        }
    }

    public double realPart()      { return re; }
    public double imaginaryPart() { return im; }

    public Complex plus(Complex c) {
        return new Complex(re + c.re, im + c.im);
    }

    // 코드 17-2 정적 팩터리(private 생성자와 함께 사용해야 한다.) (110-111쪽)
    public static Complex valueOf(double re, double im) {
        return new MyComplex(re, im);
    }

    public Complex minus(Complex c) {
        return new Complex(re - c.re, im - c.im);
    }

    public Complex times(Complex c) {
        return new Complex(re * c.re - im * c.im,
                re * c.im + im * c.re);
    }

    public Complex dividedBy(Complex c) {
        double tmp = c.re * c.re + c.im * c.im;
        return new Complex((re * c.re + im * c.im) / tmp,
                (im * c.re - re * c.im) / tmp);
    }
}

상속을 막을 수 있는 또 다른 방법

private 또는 package-private 생성자 + 정적 팩터리

    private Complex(double re, double im) {
        this.re = re;
        this.im = im;
    }
    
        // 코드 17-2 정적 팩터리(private 생성자와 함께 사용해야 한다.) (110-111쪽)
    public static Complex valueOf(double re, double im) {
        return new Complex(re, im);
    }

재정의가 가능한 클래스는 방어적인 복사를 사용해야 한다

모든 "외부에 공개하는" 필드가 final이어야 한다.

계산 비용이 큰 값을 해당 값이 필요로 할 때 (나중에) 계산하여 final이 아닌 필드에 캐시해서 쓸 수도 있다.

profile
백엔드 개발자 입니다

0개의 댓글