변경 가능성을 최소화하라(Effective Java).

Choizz·2023년 8월 22일
0

이펙티브 자바

목록 보기
13/13

이번 포스팅은 이펙티브 자바의 아이템 중 "변경 가능성을 최소화하라"에 대한 내용입니다.

보통 변경 가능성을 줄이기 위해서 불변 클래스 사용을 권장하고 있습니다.
불변 클래스는 가변 클래스 보다 설계하고 구현하기 쉽고, 사용하기 좋고, 오류가 생길 여지도 적고 안전합니다.

그래서 이번 기회에 불변 클래스에 대한 내용을 정리하려고 합니다.


불변 클래스를 만드는 규칙

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

  • 대표적으로, setter가 있습니다. 꼭 setter가 아니더라도 객체의 필드 값을 변경시키는 로직이 있어서는 안됩니다.
public class Test{

	private int x;

	public void setX(int x){
		this.x = x; //이런 로직 x
	}	
}

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

  • final을 붙이거나, 생성자에 private을 붙여 상속을 불가능하게 합니다.
  • 불변 클래스를 상속하면, 불변 클래스의 타입으로 사용할 수 있지만, 상속한 클래스가 가변 객체일 가능성이 있습니다.
//Test가 불변 객체라고 가정
public class Test2 extends Test{
    private int y;

    public Test2(final int x, final int y) {
        super(x);
        this.y = y;
    }

    public int getY() {
        return y;
    }

    public void setY(final int y) { //가변 객체가 되지만 Test의 타입을 사용할 수 있다.
        this.y = y;
    }
}

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

  • 상태를 바꾸는 것을 미연에 방지할 수 있습니다.

(4) 모든 필드는 private으로 선언한다.

  • 외부에서의 필드로의 접근을 방지하여 변경을 방지합니다.

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

  • 내부에 가변 클래스를 가지고 있을 경우, 외부의 접근을 차단해야합니다.
  • 예를 들어, 불변 클래스인 Person에 가변 클래스인 Address를 필드로 가지고 있고, getAddress()를 통해서 외부에서 가변 클래스에 접근할 수 있습니다.
public final class Person {
    private final int age;
    private final Address address;

    public Person(final int age, final Address address) {
        this.age = age;
        this.address = address;
    }

    public int getAge() {
        return age;
    }

    public Address getAddress() {
        return address;
    }
}
public class Address {
   private String city;

    public Address(final String city) {
        this.city = city;
    }

    public String getCity() {
        return city;
    }

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

}
	
  • 처음에 Person 클래스에 설정한 Address를 getter로 가지고 와서, city를 바꿀 수 있습니다.
	public static void main(String[] args) {
        Person person = new Person(13, new Address("city"));
        
        Address address = person.getAddress();
        System.out.println(person.getAddress().getCity()); // city
        
        address.setCity("new City");
        System.out.println(person.getAddress().getCity());// new city
    }

불변 클래스의 장점과 단점

장점

  • 함수형 프로그래밍에 적합합니다.
    • 피연산자에 함수를 적용한 결과를 반환할 수 있지만, 피연산자가 바뀌지 않습니다.
    • 즉, x를 넣으면 무조건 y가 나옵니다.
  • thread-safe하여 따로 동기화할 필요가 없습니다.
  • 변경되지 않기 때문에 공유가 가능합니다.
  • 불변 객체 끼리는 내부 데이터를 공유할 수 있습니다.
    • 예를 들어, BigInteger의 negate()메서드를 들 수 있습니다.
       public BigInteger negate() {
          return new BigInteger(this.mag, -this.signum); // 내부 데이터를 사용하여 다시 불변객체를 리턴합니다.
      }
  • 실패 원자성을 제공합니다.
    • 실패를 해도 본래의 데이터가 변하지 않습니다.

단점

  • 값이 다르면 반드시 별도의 객체로 만들어야하기 때문에 인스턴스가 많이 생성될 수 있습니다.

단점 해결

  • 한 꺼번에 연산을 수행하게 하여 인스턴스 생성을 줄일 수 있습니다.

    • +, -, * 연산을 한다고 가정하면, 세 번의 연산을 하기 때문에 세 개의 불변 객체가 생성됩니다.
    • 만약. 세 개의 연산을 합친다면, 하나의 불변 객체를 리턴할 수 있습니다.
  • 가변 동반 클래스를 제공하여 대처할 수 있습니다.

    • String과 StringBuilder를 예로 들 수 있습니다.

불변 클래스를 만들 때 고려할 점

  • private, private-package 생성자와 정적 팩터리 메서드를 사용할 수 있습니다.

    • 이렇게 되면, 클래스 내부에서 내부 클래스를 상속해 활용할 수 있습니다. 즉, 확장이 가능해 집니다.

       private static class MyComplex extends Complex {
      
          private MyComplex(double re, double im) {
              super(re, im);
          }
      }
  • 자주 사용되는 객체라면 static으로 캐싱을 적용할 수 있습니다.

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

    • BigInteger의 경우 불변 클래스를 의도했지만, 상속이 가능합니다.
    • 그래서, BigInteger를 상속한 클래스가 가변 객체일 수 있으므로, 방어적 복사를 사용해야 합니다.
      public static BigInteger safeInstance(BigInteger val) {
          return val.getClass() == BigInteger.class ? val : new BigInteger(val.toByteArray()); <--방어적 복사
      }
  • 외부에 공개해야하는 필드는 final이어야 한다.

    • final은 초기화시 값이 설정되어야 하기 때문에 한 번 초기화되면, 변하지 않습니다. 물론, 필드가 배열이나 가변 객체일 경우, 상태가 변할 수는 있습니다.

정리

  • 클래스를 만들 경우, 불변 클래스를 사용하는 것이 좋습니다.
  • 인스턴스를 많이 생산할 가능성이 있지만, 자주 사용되는 객체는 캐싱을하여 사용하는 방법으로 해결할 수 있습니다.

reference

profile
집중

0개의 댓글