불변 클래스
불변 클래스란 그 인스턴스의 내부 값을 수정할 수 없는 클래스다. 불변 인스턴스에 간직된 정보는 고정되어 객체가 파괴되는 순간까지 절대 달라지지 않는다.
String, 기본 타입의 박싱 된 클래스 (Wrapper Class), BigInteger, BigDecimal
불변 클래스는 가변 클래스보다 설계하고 구현하고 사용하기 쉬우며, 오류가 생길 여지도 적고 훨씬 안전하다.
기본타입의 박싱 된 클래스 (Wrapper Class)
int num = 42; Integer boxedNum = Integer.valueOf(num);
박싱 된 클래스는 기본 타입의 값을 감싸는 객체이기 때문에 객체를 다루는 메서드와 함께 사용해야 하는 경우가 있다.
반면에 기본 타입은 메모리 사용 면에서 더 효율적이기 때문에 성능이 중요한 경우에는 기본 타입을 사용하는 것이 좋다.
불변 클래스 규칙
1. 객체의 상태를 변경하는 메서드 (Setter)를 제공하지 않는다.
2. 클래스를 확장할 수 없도록 한다.
- 하위 클래스에서 부주의하거나 불순한 의도로 객체의 상태를 변하게 만드는 사태를 막아준다.
- 클래스를 상속하지 못하게 하는 가장 쉬운 방법은 final 클래스로 선언하는 것이지만 더 유연한 방법이 있다.
모든 생성자를 private 혹은 package-private으로 만들고 public 정적 팩터리를 제공하자.
왜 정적 팩터리 방식이 더 유연할까?
이 방식은 다수의 구현 클래스를 활용한 유연성을 제공한다.
인터페이스나 추상 클래스를 정의하고, 그 인터페이스나 추상 클래스를 구현하는 다수의 구현 클래스를 만들 때 각각의 구현 클래스를 생성할 때마다 생성자를 호출하는 것은 매우 번거롭고 유지보수도 어렵다.
이때 정적 팩터리 메서드를 사용하면 객체 생성 과정을 더욱 유연하게 제어할 수 있다.
예를 들어 Map 인터페이스를 구현하는 클래스인 HashMap, TreeMap, LinkedHashMap에서 이들 클래스를 생성할 때마다 생성자를 호출하는 대신 Map 인터페이스에 정의된 정적 팩터리 메서드인 Map.of()를 사용하면 객체 생성 과정을 더욱 유연하게 제어할 수 있다.
3. 모든 필드를 final로 선언한다.
- 시스템이 강제하는 수단을 이용해 설계자의 의도를 명확히 드러내는 방법이다.
- 새로 생성된 인스턴스를 동기화 없이 다른 스레드로 건네도 문제없이 동작하게끔 보장한다.
4. 모든 필드를 private으로 선언한다.
- 필드가 참조하는 가변 객체를 클라이언트에서 직접 접근해 수정하는 일을 막아준다.
5. 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다.
- 클래스에 가변 객체를 참조하는 필드가 하나라도 있다면 클라이언트에서 그 객체의 참조를 얻을 수 없도록 해야 한다.
- 이런 필드는 절대 클라이언트가 제공한 객체 참조를 가리키게 해서는 안되며, getter가 그 필드를 그대로 반환해서도 안된다.
- 생성자, getter, readObject에서 방어적 복사(새로운 객체를 생성하여 복사)를 수행하자.
가변 객체 address를 참조하는 필드를 가지는 Student 클래스를 만들어보자.
각 클래스들은 모두 getter, setter, constructor가 선언되어 있다.
Student의 필드는 가변 객체인 Address를 그대로 참조하고 있기 때문에 Address의 값이 변경되면 Student의 값도 변경된다. 이러한 불상사를 막으려면 Student의 필드에서 Address를 참조할 때 새로운 객체를 생성하여 방어적 복사를 수행해 주자.
값이 변하지 않는다!
함수형 프로그래밍
해당 메서드는 인스턴스 자신은 수정하지 않고 새로운 Complex 인스턴스를 만들어 반환한다.
이 처럼 피연산자에 함수를 적용해 그 결과를 반환하지만, 피연산자 자체는 그대로인 프로그래밍 패턴을 함수형 프로그래밍이라고 한다.
add와 같이 동사가 아니라 plus 같은 전치사를 사용한 점에서도 객체의 값을 변경하지 않는다는 사실을 강조하려는 의도이다.
함수형 프로그래밍을 사용하면 코드에서 불변이 되는 영역의 비율이 높아지는 장점을 누릴 수 있다.
장점
1. 근본적으로 스레드 안전하여 따로 동기화할 필요가 없다.
- 여러 스레드가 동시에 사용해도 절대 훼손되지 않는다.
2. 안심하고 공유할 수 있다.
- 어떤 스레드도 다른 스레드에 영향을 줄 수 없으니 안심하고 공유할 수 있다.
- 복사를 해도 수정할 수 없기 때문에 방어적 복사도 필요 없다.
- 가변 객체를 참조하더라도 값을 변경할 수 없기 때문에 내부 데이터를 공유해도 된다.
단점
값이 다르면 반드시 독립된 객체로 만들어야 한다.
그렇기 때문에 값의 가짓수가 많다면 이들을 모두 만드는데 큰 비용을 치러야 한다.
예를 들어 백만 비트짜리 BigInteger에서 비트 하나를 바꿔야 한다고 했을 때
원본과 단지 한 비트만 다른 백만 비트짜리 인스턴스를 새로이 생성해야 한다.
이러한 단점은 원하는 객체를 완성하기까지의 단계가 많고, 그 중간 단계에서 만들어진 객체들이 모두 버려진다면 성능 문제는 더욱 커진다.
다단계 연산 예측
위의 단점을 극복할 방법 중 한 가지인 다단계 연산들을 예측하여 기본 기능으로 제공하는 방법이다.
다단계 연산을 기본으로 제공한다면 더 이상 각 단계마다 객체를 생성하지 않아도 된다.
다단계 연산
다단계 연산이란
한 개의 객체에서 다른 객체로 이어지는 연속적인 메서드 호출을 의미한다.
예를 들어 student.getAddress(). getCity(). getPopulation()과 같은 코드가 있을 때 이는 Person 객체의 Address 객체의 City 객체의 인구수를 반환하는 다단계 연산이다.
이러한 연산은 객체를 참조하는 작업이 중복될 가능성이 높기 때문에 성능상 이슈가 발생할 수 있다.
String에서 가변 동반 클래스 StringBuilder가 제공된다.
결론
- 클래스는 꼭 필요한 경우가 아니라면 불변이어야 한다.
- 불변으로 만들 수 없는 클래스라도 변경할 수 있는 부분을 최소한으로 줄이자.
- 다른 합당한 이유가 없다면 모든 필드는 private final이어야 한다.
- 생성자는 불변식 설정이 모두 완료된, 초기화가 완벽히 끝난 상태의 객체를 생성해야 한다.
'Java > Effective Java' 카테고리의 다른 글
[Effective Java] Item 19. 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라 (0) | 2023.03.28 |
---|---|
[Effective Java] Item 18. 상속보다는 컴포지션을 사용하라 (0) | 2023.03.25 |
[Effective Java] Item 16. public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라 (2) | 2023.03.21 |
[Effective Java] Item 15. 클래스와 멤버의 접근 권한을 최소화하라 (0) | 2023.03.21 |
[Effective Java] Item 14. Comparable을 구현할지 고려하라 (1) | 2023.03.21 |