불변성(immutable)이 무엇인지 설명하기 위해, 먼저 우리는 가변성이 무엇인지 이해해야 한다.
- 가변성(muttable) : 정체성이 동일하게 유지되는 동안 엔티티의 상태를 변경할 수 있는 가능성을 의미한다.
불변성은 그 반대이다. 일단 엔티티가 존재하면, 그 상태는 절대 변하지 않을 것이다.
참고: Differences between entity and object are
No. Entity Object 1. 엔티티는 다른 객체와 구별할 수 있는 실시간 객체 객체는 취해야 할 모든 속성과 작업을 가진 엔티티 2. 엔티티는 속성을 포함 객체에는 생명 주기, 식별자가 존재 3. 엔티티는 고유하게 식별 가능한 객체 객체는 식별자를 사용하여 식별 4. 모든 단체는 식별 목적으로 기본 키를 보유 객체는 기본 키로 할당되지 않는다. 5. 엔티티는 관계형 데이터베이스의 일부 객체는 객체 지향 데이터베이스의 일부 6. 엔티티는 E-R 다이어그램을 사용하여 직사각형 모양으로 표시된다 객체는 그래픽으로 표현되지 않는다. 7. Attribute는 엔티티의 property 상속, 캡슐화, 다형성 및 추상화는 객체의 일부 8. 예: Example: Computer, Software. 예: Example: Minimum age to vote is 18.
이러한 개념은 OOP에서 어떻게 표현되는가?
객체는 세 가지 특징을 가지고 있다:
그 특징과 차이점을 이해하는 것은 매우 중요하다.
자동차 객체를 설계시
그렇다면 우리는 물체의 상태를 어떻게 표현하는가?
일반적으로 받아들여지는 응답은 객체의 상태가 필드로 표현된다는 것이다.
객체가 불변하기 위해선 필드가 모든 상황에서 바꿀 수 없다는 것을 의미한다.
불변성은 객체의 필드를 변경할 수 없다는 것을 의미 -> 즉, 객체는 많은 곳에서 참조될 수 있다.
- 누구나 모든 공개 메서드를 호출하거나 공개 필드를 수정할 수 있다.
- 그래서 첫 번째 단계로 모든 필드를 비공개로 만들고, 메소드를 통해서만 접근할 수 있도록 하는 것이다.
- 필드가 공개된다면, 그것을 통제할 수 없고 누구나 그것을 바꿀 수 있기 때문
또 다른 단계는 모든 필드를 최종적(final)으로 만드는 것이다.
일단 필드가 생성되면 아무도 그것을 변경할 수 없다는 것을 보장한다.
필드가 원시적(primitive)이라면, 비공개로 만들고 최종적으로 만드는 것으로 충분하다. 그러나, 필드가 가변 물체라면, 이것은 그것을 지키기에 충분하지 않다.
그 필드는 실제로 객체가 아니며, 객체가 저장되는 위치를 나타내는 참조일 뿐. 즉, 최종적이고 비공개이기 때문에,
그 참조는 변경할 수 없지만 해당 객체에 접근할 수 있으며 메서드를 호출할 수 있다. 그리고 객체가 변경가능하다면, 그 상태는 바뀔 수 있다.
이 문제를 어떻게 해결할 수 있을까?
getter를 통해 이 객체를 반환해야 한다면, 방어 복사본(Defensive copy)을 만들고 대신 복사본을 반환해야 한다.
Integer 객체는 불변하지만, list는 그렇지 않다.
getter를 통해 반환해야 하는 경우
setter의 경우
클래스가 생성자의 매개 변수로 가변 객체를 받으면 필드에 할당되어야 하며, 다시 방어 복사본을 만들고 필드에 복사본을 할당해야 한다.
하지만 방어 사본을 반환하는 것만으로는 충분하지 않다.
생성자에서 변수 객체를 매개 변수로 받고 받은 대로 필드에 할당한다고 가정해 보자.
필드는 private, final이며 getter에서 방어 복사본을 만들고 사본을 반환한다.
하지만 생성자에 복사하지 않았다면 객체는 다른 곳에서 참조될 수 있으며, 수정되어 객체 상태의 변경을 초래할 수 있다.
만약 우리의 getter들이 무시된다면?
- 모든 필드를 비공개 및 최종으로 선언
- 필드가 가변 객체라면, 방어 복사본을 만들고 반환할 것
- setter method를 노출금지
- 클래스가 (생성자에서) 필드에 할당되어야 하는 매개 변수로 가변 객체를 받으면, 방어 복사본을 만들고 필드에 복사본을 할당할 것
- getter method 오버라이딩을 허용하지 말 것
자바는 이미 우리에게 불변의 객체를 많이 제공한다.
Integer, Byte, Long, Float, Double, Character, Boolean 및 Short와 같은 모든 기본 래퍼 클래스는 Java에서 불변한다.
객체에 예를 들어 정수인 필드가 있다면, 비공개로 설정하고 최종적으로 보호하기에 충분하다.
public class ImmutableObjectExample {
private final int a;
private final Integer b;
private final List<Integer>c;
public ImmutableObjectExample(int a, Integer b, List<Integer> c) {
this.a = a;// a is a primitive, so it doesn’t need a defensive copy
this.b = b; // b is Integer and Integer is an immutable object. Doesn’t
// need a defensive copy also;
this.c = new ArrayList<>(c);
}
}
방어 복사본을 만드는 방법이 생성자에서 getter와 다르다.
(list에 대한 방어적인 복사본을 만드는 방법이 하나도 없다.)
list의 방어 복사본을 만들 때 가장 중요한 것은 그것이 가지고 있는 요소이다.
목록의 요소가 불변의 객체라면 방어 복사본을 만들기 위해, 동일한 요소를 넣는 다른 목록을 만들어야 하며 이를 위한 방법들은 다음과 같다.
복사본은 가변 객체이며, 수정할 수 있고, 요소를 추가하거나, 요소를 제거할 수 있지만, 원래 목록에는 영향을 미치지 않는다.
그리고 복사된 목록에 원본 목록과 동일한 객체가 있더라도, 이 경우 정수인 요소는 불변하므로 여러 곳에서 참조되더라도 수정할 수 없다.
기억해라. 생성자와 Getter에서도 방어 복사본을 만들어야 또 다른 옵션이 있다.
생성자의 매개 변수를 기반으로 불변의 객체를 만들고 대신 저장할 수 있다.
그런 다음 변경 가능한 객체를 받더라도 객체의 필드는 불변의 복사본이 될 것이며 직접 반환할 수 있게 된다.
// 최종 접근자(final accessor)를 클래스에 설정하는 것 유일한 방법
// 메서드 오버라이딩을 사용해선 안 된다.
public class ImmutableObjectExample {
private final int a;
private final Integer b;
private final List<Integer>c;
public ImmutableObjectExample(int a, Integer b, List<Integer> c) {
this.a = a;// primitive -> 불변이므로 불필요
this.b = b; // Integer는 불변이므로 불필요
this.c = new ArrayList<>(c); // 리스트가 불변 객체가 아니기 때문에 방어 복제본을 만들고
// 필드에 할당해야 한다.
}
public int getA() {
return a;
}
public Integer getB() {
return b;
}
public List<Integer> getC() {
//defensive copy 반환 필요
return c.stream().collect(Collectors.toList());Collectors.toList()
}
}
어쨌든, list는 list이다.
getter는 list 인터페이스를 구현하는 객체를 반환하며
물론 추가 또는 제거와 같은 list의 메서드가 있지만,
이러한 방법 중 하나를 호출하려고 하면 예외가 발생한다.
이것이 변경할 수 없는 list가 구현되는 방법이다.
필드가 변경할 수 있다면, 방어 복사본으로 보호해야 한다.
원시적이거나 불변의 객체라면, 접근자를 private, final로 설정하면 충분하다.
불변의 객체를 변경할 수 없지만, 대신 그것을 기반으로 다른 객체를 만들 수 있다.
유일한 할 수 있는 것은 또 다른 방어 복사본이지만, 이번에는 모든 객체를 복사하고 변경을 추가해야 한다.
예제를 살펴보면, 불변 클래스의 인스턴스가 있다면, 기존 인스턴스의 정보로 클래스의 생성자를 호출하여 복사본을 만들 수 있다.
getter 메서드는 원래 객체 필드의 방어 복사본을 반환하므로 해당 getter가 반환한 객체를 사용하여 복사본을 안전하게 구성한다.
ImmutableObjectExample originalInstance = new ImmutableObjectExample(10, 20, Arrays.asList(0, 1,2));
첫 번째 필드가 1 증가한 복사본을 만들고 싶다면, 다음과 같이 구성할 수 있다.
ImmutableObjectExample copyInstance = new ImmutableObjectExample(
originalInstance.getA() + 1,
originalInstance.getB(),
originalInstance.getC());
매번 생성자를 호출하는 것은 꽤 못생겨 보일 수 있는데,
꼭 그럴 필요 없이 생성자 호출을 위한 몇 가지 factory method을 가짐으로써 코드를 간결하게 할 수 있다.
불변의 클래스 구현할 때 더 큰 노력을 기울여야 할 것 같다.
왜 그래야 하는가?
immutable class 구현으로 얻는점
- 불변 객체가 살아있는 동안 동일하게 유지될 것이라는 것을 보장
- 한 객체가 누구에 의한건지 모른 채로 갑자기 변경된 적 있나요? 예를 들면..
public Object method(MyClass a) {
// do something
a.setSomething(something);
// etc etc
return anObject;
}
이것은 실망스러운 놀라움의 근원 중 하나이다.
매개 변수를 수정하는 것은 객체의 상태에 대해 추론하기가 매우 어렵기 때문에 안티패턴이다. 그리고 이러한 안티 패턴은 계속 일어날 것이다.
- 변경할 수 없는 불변의 객체는 스레드로부터 안전하다.
- 불변 객체가 얼마나 많은 스레드에서 사용되든, 그것은 스레드로부터 안전하다.
- 불변 객체는 방어 복사본을 사용하여 가변 필드에 대한 접근을 제공한다.
- 그래서 그 필드 중 하나를 사용하는 각 스레드는 실제로 방어 복사본을 사용합니다.
- 그 객체를 수정하는 일부 코드 블록을 동기화하기 위해 탈출했다는 두려움에서 자유로울 수 있다.
불변 객체가 우리 코드에 가져오는 안전 외에 또 다른 장점은 가비지 수집 최적화이다.
Immutable 객체와 GC의 연결
변경되지 않은 객체는 GC에 매우 유리하다는 것이 요점이라고 한다.
완벽한 것은 없으며, 불변의 물체를 포함하여 장점만이 존재하는 단일 개념은 없다.
불변 객체의 주요 문제는 성능이다.
(무언가를 변경하는 대신 새로운 것을 만들어야 하기 때문)
애플리케이션이 많은 변경을 한다면,
기존 객체를 수정하는 대신 많은 객체를 만드는 것이 최선의 방법이 아니다. 그래서 우리는 가변성에서 벗어날 수 없지만, 최소 수준으로 유지하는 것이 좋다고 할 수 있다.
그러나, 불변 객체를 더 최적으로 만들기 위해 우리가 할 수 있는 몇 가지가 있다.
불변의 필드 유지
로컬 메소드에서 가변 가능한 객체를 사용
public String basicConcatenation(String[] args){
String result = "";
for (String string : strings) {
result += string;
}
}
문자열은 불변의 객체이다.
두 문자열을 "+" 연산자와 연결하면 실제로 다른 문자열이 생성된다.
그래서, 위의 구현을 사용하여, 우리는 모든 목록 항목에 대해 많은 문자열 객체를 만들고 있다.
한 문자열만 수정하는 것이 더 효율적이지 않을까요?
맞다. 그리고 우리는 StringBuilder라는 String의 변경 가능한 변형을 사용하여 이것을 할 수 있습니다.
public String efficientConcatenation(String[] args){
// create a mutable object
StringBuilder builder = new StringBuilder();
for (String string : strings) {
builder.append(string);
}
// 가변 객체를 불변 객체로 변환하여 반환
return builder.toString();
}
단점을 낮게 유지하면서 장점을 키우는 것의 여부는 불변 객체를 언제 어떻게 사용할지 선택하는 우리의 지혜에 달려 있다.
성능이 떨어질 수 있지만, 변경은 어떤 형태로든 다른 객체의 생성을 의미하기 때문에, "명확한 불변성"이라는 큰 이점을 가져온다.
불변의 객체를 여러 곳에서 자유롭게 공유하거나 멀티스레드 환경에서 사용할 수 있다.
무슨 일이 있어도, 그들의 상태는 절대 변하지 않을 것이고 코드는 훨씬 더 안전할 것이며, 그것에 대해 추론하기가 더 쉬울 것이다.