-
0. 들어가며이번 아이템은 불변 클래스(Immutable Class) 에 대한 내용입니다. 클래스를 불변으로 만들기 위한 규칙과 장단점 등에 대해 알아볼 예정입니다. 1. 불변 클래스
생성된 시점에 상태가 확정되어, 소멸되기 전까지 달라지지 않습니다. 이렇게 불변 클래스로 만들면 설계, 구현, 사용이 쉬워지며 오류 발생 여지도 줄어들게 됩니다. 이 대표적인 예가 바로
아래에서 불변 클래스의 예시가 필요할 때 이 두 클래스를 비교하며 살펴보겠습니다. 1.1 불변 클래스의 다섯가지 규칙이펙티브 자바에서는 이러한 불변 객체를 만들기 위해서는 다음 다섯가지 규칙을 만족해야 한다고 합니다.
1) 객체의 상태를 변경하는 메서드를 제공하지 않는다.일반적으로 객체는 자바 빈 규약의 ‘getter, setter 를 제공해야 한다’ 라는 규약에 따라 getter, setter 를 제공합니다. 그래서 이전에는 여러 자바 기술들이 자바 빈 규약에 따라 설계되었습니다. 하지만 불변 객체의 중요성이 대두되며 setter 사용을 지양하는 흐름으로 변화되었는데요. Date 클래스는 다음과 같은 많은 변경 메서드들이 있었습니다. 하지만 변경할 수 없고, 멀티 스레드에서도 안전한 날짜 & 시간 관련 클래스가 필요했습니다. 그래서 등장한 LocalDate 클래스는 setter 를 제공하지 않는 불변 클래스 입니다. 2) 클래스를 확장할 수 없도록 한다.클래스를 상속하여 메서드를 오버라이딩 하거나 추가하면 변경이 가능하도록 변할 수 있습니다. 이 때문에 하위 클래스에서 객체의 상태를 변경시킬 수 없도록 상속을 방지하는것이 필요한데요. 그래서 LocalDate 클래스는 final 클래스로 선언하여 상속을 방지하고 있습니다. 3) 모든 필드를 final 로 선언한다.먼저, final 키워드를 이용하면 언어 차원에서 변경을 제한하기 때문에 강력합니다. 그리고 자바 언어에서는 final 로 선언된 필드는 멀티스레드에서도 안전하다고 합니다. (Java Language Specification, final field) Date 클래스 내부적으로 CalendarDate 라는 클래스가 사용되는데요. CalendarDate 클래스의 경우 모든 필드가 변경 가능한 필드인데 반해, LocalDate 는 모두 final 이 붙어있습니다. 4) 모든 필드를 private 으로 선언한다.이펙티브자바에서는 가능한 모든 필드를 private 으로 선언할 것을 요구합니다. private 으로 선언하면 클라이언트가 직접 접근하여 수정하는 일을 방지할 수 있습니다. 하지만 public 을 붙여도 final 을 붙여도 불변이 되기는 하는것 아니냐는 생각을 할 수 있는데요. 이에 대해서 책에서는 public final 필드를 수정할 경우 파급효과가 크기 때문에 권하지는 않는다고 합니다. 5) 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다.클래스 내에 가변 객체가 있다면 이를 절대 외부에 노출하면 안된다고 합니다. 그리고 노출 하더라도 복사를 통해 전달해야하며 반드시 방어적 복사를 수행해야 합니다. 이를 Date 와 LocalDate 클래스를 통해 살펴보겠습니다. // Date.setDate 메서드
public void setDate(int date) {
getCalendarDate().setDayOfMonth(date);
}
// CalendarDate.setDayOfMonth 메서드
public CalendarDate setDayOfMonth(int date) {
if (dayOfMonth != date) {
dayOfMonth = date;
normalized = false;
}
return this;
}
// ---------------------------------------------------------------------------
// LocalDate.plusDays 메서드
public LocalDate plusDays(long daysToAdd) {
if (daysToAdd == 0) {
return this;
}
long dom = day + daysToAdd;
if (dom > 0) {
if (dom <= 28) {
return new LocalDate(year, month, (int) dom);
} else if (dom <= 59) { // 59th Jan is 28th Feb, 59th Feb is 31st Mar
long monthLen = lengthOfMonth();
if (dom <= monthLen) {
return new LocalDate(year, month, (int) dom);
} else if (month < 12) {
return new LocalDate(year, month + 1, (int) (dom - monthLen));
} else {
YEAR.checkValidValue(year + 1);
return new LocalDate(year + 1, 1, (int) (dom - monthLen));
}
}
}
long mjDay = Math.addExact(toEpochDay(), daysToAdd);
return LocalDate.ofEpochDay(mjDay);
}
// LocalDate.ofEpochDay 메서드
public static LocalDate ofEpochDay(long epochDay) {
... 생략
return new LocalDate(year, month, dom);
} 위는 두 클래스의 날짜를 변경하는 메서드 인데요. Date 클래스는 내부 캘린더 객체의 날짜 필드를 변경하는 메서드를 호출합니다. 반면에 LocalDate 의 경우에는 return 문에서 항상 새로운 객체를 생성하여 반환합니다. 1.2 불변 클래스의 특징
1) 불변 객체는 단순함가변 객체는 상태가 변할 수 있기 때문에, 이를 고려하여 여러 안전장치가 필요합니다. 반면에 불변 객체는 한번 설정된 상태를 그대로 간직합니다. 그 때문에 불변 객체는 내부에서나 외부에서나 관련된 로직이 단순해질 수 밖에 없습니다. 이는 다음 특징인 2번과도 연관이 깊습니다. 2) 멀티 스레드 환경에서도 동기화할 필요가 없음불변 객체는 값이 변경되면 새로운 객체로 반환하기 때문에 멀티 스레드 환경에서도 안전합니다. // 가변 객체
class MutablePoint {
private int x;
private int y
public MutablePoint(int x, int y) {
this.x = x;
this.y = y;
}
getter, setter ...
}
// 불변 객체
class ImmutablePoint {
private int x;
private int y;
public ImmutablePoint(int x, int y) {
this.x = x;
this.y = y;
}
getter ...
public ImmutablePoint setX(int x) {
return new ImmutablePoint(x, this.y);
}
public ImmutablePoint setY(int y) {
return new ImmutablePoint(this.x, y);
}
} 위 처럼 가변, 불변의 특징을 가진 두 Point 클래스가 있습니다. MutablePoint 의 경우에는 멀티 스레드 환경에서 getter, setter 에서 race condition 이 발생할 수 있습니다. 반면에 ImmutablePoint 의 경우에는 setter 에서 항상 다른 객체를 반환합니다. 그래서 멀티 스레드 환경에서도 안전하게 객체를 다룰 수 있습니다. 3) 원자성을 제공원자성은 위 2번의 예제에서 같이 설명이 가능합니다. ImmutablePoint 의 setter 메서드의 경우 각 작업은 동일한 조건이라면 동일한 결과를 반환합니다. 4) 값이 달라지면, 별도의 객체를 만들어야 한다는 것이번 예제 또한 2번의 예제에서 잘 설명되어있습니다. ImmutablePoint 는 setter 에 의해 상태가 변경되면, 새로운 객체를 생성해서 반환합니다. 하지만, 만약 객체를 생성하는 과정의 비용이 크다면 어떨까요?
등등의 경우에는 잠재적인 성능 문제를 야기할 수 있습니다. 그래서 이펙티브 자바에서는 ECT. 다단계 연산
다단계 연산은 동반클래스를 두는 방식으로 많이 구현하며 대표적인 예로 아래 클래스들이 있습니다.
BigInteger 의 내부 연산 과정에서 값이 어려번 바뀌는 경우가 있습니다. 이런 경우에 MutableBigInteger 를 이용해서 새로운 객체를 생성하지 않고 값을 변경한 다음 최종 결과를 다시 BigInteger 로 변환하여 반환하기도 합니다.
|
Beta Was this translation helpful? Give feedback.
Replies: 3 comments
-
불변 클래스 정리 중 5) 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다.에서 String의 replace 메서드 String str = new String("abc");
str.replace("a", "z");
System.out.println(str); replace 메서드는 str 의 a를 z로 바꾸는 메서드 입니다. 결과를 보면 원하는 결과대로 이루어지 않고 원문 그대로 입니다. public String replace(CharSequence target, CharSequence replacement) {
String tgtStr = target.toString();
String replStr = replacement.toString();
int j = indexOf(tgtStr);
if (j < 0) {
return this;
}
int tgtLen = tgtStr.length();
int tgtLen1 = Math.max(tgtLen, 1);
int thisLen = length();
int newLenHint = thisLen - tgtLen + replStr.length();
if (newLenHint < 0) {
throw new OutOfMemoryError();
}
StringBuilder sb = new StringBuilder(newLenHint);
int i = 0;
do {
sb.append(this, i, j).append(replStr);
i = j + tgtLen;
} while (j < thisLen && (j = indexOf(tgtStr, j + tgtLen1)) > 0);
return sb.append(this, i, thisLen).toString();
} replace 메서드의 구현을 보니 기존 객체를 건드리지 않고 StringBuilder를 이용해 새로운 문자열 객체를 만들어 반환하는 것을 확인할 수 있습니다. (불변이니 당연히 해당 객체를 건들면 안되니...) 따라서 변수로 다시 받아줘야 새롭게 만들어져 반환한 객체를 참조할 수 있고 이렇게 코드를 수정하면 String str = new String("abc");
str = str.replace("a", "z");
System.out.println(str); 원하는 결과로 나오는 것을 확인할 수 있었습니다. 저는 알고리즘 문제를 풀면서 replace 메서드를 제법 쓴 경험이 있는데 이러한 내부 동작을 잘 알지 못하고 그냥 replace만 쓰고 제대로 동작하지 않는다며 시간을 많이 낭비한 경험이 있습니다. @jinan159 님께서 불변 클래스의 예시로 드신 LocalDate 와 마찬가지로 String 클래스 또한 불변 클래스이고 이러한 불변클래스의 메서드들은 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 하기 위해 return을 자신 객체가 아닌 새로운 객체를 생성하여 반환하는 경우가 많을 수 있으니 참고 하시면 좋을 것 같습니다. 정리하신글 잘 읽었습니다. 감사합니다. |
Beta Was this translation helpful? Give feedback.
-
정리 감사합니다.! |
Beta Was this translation helpful? Give feedback.
-
1.2 불변 클래스의 특징
이 부분 예시 |
Beta Was this translation helpful? Give feedback.
불변 클래스 정리 중
5) 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다.
에서
LocalDate 의 경우에는 return 문에서 항상 새로운 객체를 생성하여 반환
하여 자신 외에 내부의 가변 컴포넌트에 접근할 수 없도록 한 부분을 보다가 String 객체의 멤버 메서드의 return 또한 이러한 경우가 제법 있었던 것으로 기억났습니다.String의 replace 메서드
replace 메서드는 str 의 a를 z로 바꾸는 메서드 입니다. 결과를 보면
원하는 결과대로 이루어지 않고 원문 그대로 입니다.